├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ └── bug_report.md
├── stale.yml
└── workflows
│ ├── codeql-analysis.yml
│ └── docker-ghcr.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── check_and_gen.py
├── config.py
├── docker
└── download_chromedriver.py
├── docker_check_and_gen.sh
├── docker_hunt.sh
├── ghunt.py
├── lib
├── __init__.py
├── banner.py
├── calendar.py
├── gmaps.py
├── listener.py
├── metadata.py
├── modwall.py
├── os_detect.py
├── photos.py
├── search.py
├── utils.py
└── youtube.py
├── modules
├── doc.py
├── email.py
├── gaia.py
└── youtube.py
├── profile_pics
└── .keep
├── requirements.txt
└── resources
└── .gitkeep
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: mxrch
2 | custom: https://www.blockchain.com/btc/address/362MrYHLLMzWBbvBG7K5yzF18ouxMZeNge
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **System (please complete the following information):**
27 | - OS
28 | - Python version
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-stale - https://github.com/probot/stale
2 |
3 | # Number of days of inactivity before an Issue or Pull Request becomes stale
4 | daysUntilStale: 30
5 |
6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
8 | daysUntilClose: 21
9 |
10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
11 | onlyLabels: []
12 |
13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
14 | exemptLabels:
15 | - pinned
16 | - security
17 | - bug
18 | - keep
19 |
20 | # Set to true to ignore issues in a project (defaults to false)
21 | exemptProjects: false
22 |
23 | # Set to true to ignore issues in a milestone (defaults to false)
24 | exemptMilestones: false
25 |
26 | # Set to true to ignore issues with an assignee (defaults to false)
27 | exemptAssignees: false
28 |
29 | # Label to use when marking as stale
30 | staleLabel: stale
31 |
32 | # Comment to post when removing the stale label.
33 | # unmarkComment: >
34 | # Your comment here.
35 |
36 | # Comment to post when closing a stale Issue or Pull Request.
37 | # closeComment: >
38 | # Your comment here.
39 |
40 | # Limit the number of actions per hour, from 1-30. Default is 30
41 | limitPerRun: 30
42 |
43 | # Limit to only `issues` or `pulls`
44 | # only: issues
45 |
46 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
47 | pulls:
48 | daysUntilStale: 60
49 | markComment: >
50 | This pull request has been automatically marked as stale because it has not had
51 | activity on the last 60 days. It will be closed in 7 days if no further activity occurs. Thank you
52 | for your contributions.
53 |
54 | issues:
55 | markComment: >
56 | This issue has been automatically marked as stale because it has not had
57 | recent activity on the last 18 days. It will be closed in 6 days if no further activity occurs.
58 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | #Don't ask me what any of this means, this file was generated with Github's web-GUI
2 |
3 | # For most projects, this workflow file will not need changing; you simply need
4 | # to commit it to your repository.
5 | #
6 | # You may wish to alter this file to override the set of languages analyzed,
7 | # or to provide custom queries or build logic.
8 | name: "CodeQL"
9 |
10 | on:
11 | push:
12 | branches: [master]
13 | pull_request:
14 | # The branches below must be a subset of the branches above
15 | branches: [master]
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # Override automatic language detection by changing the below list
26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
27 | language: ['python']
28 | # Learn more...
29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v2
34 | with:
35 | # We must fetch at least the immediate parents so that if this is
36 | # a pull request then we can checkout the head.
37 | fetch-depth: 2
38 |
39 | # If this run was triggered by a pull request event, then checkout
40 | # the head of the pull request instead of the merge commit.
41 | - run: git checkout HEAD^2
42 | if: ${{ github.event_name == 'pull_request' }}
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/docker-ghcr.yml:
--------------------------------------------------------------------------------
1 | name: 'Build & Push Image'
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | name: 'Build'
12 | runs-on: ubuntu-latest
13 | env:
14 | IMAGE_NAME: ghunt
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 |
19 | - name: Set up Docker Buildx
20 | uses: docker/setup-buildx-action@v1
21 |
22 | - name: Login to GitHub Container Registry
23 | uses: docker/login-action@v1
24 | with:
25 | registry: ghcr.io
26 | username: ${{ github.repository_owner }}
27 | password: ${{ secrets.GHCR_TOKEN }}
28 |
29 | - name: Build and push
30 | uses: docker/build-push-action@v2
31 | with:
32 | context: .
33 | push: ${{ GitHub.event_name != 'pull_request' }}
34 | tags: ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | lib/__pycache__/
3 | modules/__pycache__/
4 | profile_pics/*.jpg
5 | resources/
6 | chromedriver
7 | chromedriver.exe
8 | data.txt
9 | login.py
10 | .DS_Store
11 | debug.log
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8.6-slim-buster
2 |
3 | ARG UID=1000
4 | ARG GID=1000
5 |
6 | WORKDIR /usr/src/app
7 |
8 | RUN groupadd -o -g ${GID} -r app && adduser --system --home /home/app --ingroup app --uid ${UID} app && \
9 | chown -R app:app /usr/src/app && \
10 | apt-get update && \
11 | apt-get install -y curl unzip gnupg && \
12 | curl -sS -o - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
13 | echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list && \
14 | apt-get update && \
15 | apt-get install -y google-chrome-stable && \
16 | rm -rf /var/lib/apt/lists/*
17 |
18 | COPY --chown=app:app requirements.txt docker/download_chromedriver.py ./
19 |
20 | RUN python3 -m pip install --no-cache-dir -r requirements.txt && \
21 | python3 download_chromedriver.py && chown -R app:app /usr/src/app
22 |
23 | COPY --chown=app:app . .
24 |
25 | USER app
26 |
27 | ENTRYPOINT [ "python3" ]
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 
4 |
5 |  
6 | # Description
7 | GHunt is a modulable OSINT tool designed to evolve over the years, and incorporates many techniques to investigate Google accounts, or objects.\
8 | It currently has **email**, **document**, **youtube** and **gaia** modules.
9 |
10 | ## What can GHunt find ?
11 |
12 | 🗺️ **Email** module:
13 | - Owner's name
14 | - Gaia ID
15 | - Last time the profile was edited
16 | - Profile picture (+ detect custom picture)
17 | - If the account is a Hangouts Bot
18 | - Activated Google services (YouTube, Photos, Maps, News360, Hangouts, etc.)
19 | - Possible YouTube channel
20 | - Possible other usernames
21 | - Google Maps reviews (M)
22 | - Possible physical location (M)
23 | - Events from Google Calendar (C)
24 | - Organizations (work & education) (A)
25 | - Contact emails (A)
26 | - Contact phones (A)
27 | - Addresses (A)
28 | - ~~Public photos (P)~~
29 | - ~~Phones models (P)~~
30 | - ~~Phones firmwares (P)~~
31 | - ~~Installed softwares (P)~~
32 |
33 | 🗺️ **Document** module:
34 | - Owner's name
35 | - Owner's Gaia ID
36 | - Owner's profile picture (+ detect custom picture)
37 | - Creation date
38 | - Last time the document was edited
39 | - Public permissions
40 | - Your permissions
41 |
42 | 🗺️ **Youtube** module:
43 | - Owner's Gaia ID (through Wayback Machine)
44 | - Detect if the email is visible
45 | - Country
46 | - Description
47 | - Total views
48 | - Joined date
49 | - Primary links (social networks)
50 | - All infos accessible by the Gaia module
51 |
52 | 🗺️ **Gaia** module:
53 | - Owner's name
54 | - Profile picture (+ detect custom picture)
55 | - Possible YouTube channel
56 | - Possible other usernames
57 | - Google Maps reviews (M)
58 | - Possible physical location (M)
59 | - Organizations (work & education) (A)
60 | - Contact emails (A)
61 | - Contact phones (A)
62 | - Addresses (A)
63 |
64 | The features marked with a **(P)** require the target account to have the default setting of `Allow the people you share content with to download your photos and videos` on the Google AlbumArchive, or if the target has ever used Picasa linked to their Google account.\
65 | More info [here](https://github.com/mxrch/GHunt#%EF%B8%8F-protecting-yourself).
66 |
67 | Those marked with a **(M)** require the Google Maps reviews of the target to be public (they are by default).
68 |
69 | Those marked with a **(C)** require user to have Google Calendar set on public (default it is closed).
70 |
71 | Those marked with a **(A)** require user to have the additional info set [on profile](https://myaccount.google.com/profile) with privacy option "Anyone" enabled.
72 |
73 | # Screenshots
74 |
75 |
76 |
77 |
78 | ## 📰 Latest news
79 | - **02/10/2020** : Since a few days ago, Google returns a 404 when we try to access someone's Google Photos public albums, we can only access it if we have a link to one of their albums.\
80 | Either this is a bug and this will be fixed, either it's a protection that we need to find how to bypass.
81 | - **03/10/2020** : Successfully bypassed. 🕺 (commit 01dc016)\
82 | It requires the "Profile photos" album to be public (it is by default)
83 | - **20/10/2020** : Google WebArchive now returns a 404 even when coming from the "Profile photos" album, so **the photos scraping is temporary (or permanently) disabled.** (commit e762543)
84 | - **25/11/2020** : Google now removes the name from the Google Maps profile if the user has 0 reviews (or contributions, even private). I did not find a bypass for the moment, so **all the help in the research of a bypass is appreciated.**
85 | - **20/03/2021** : Successfully bypassed. 🕺 (commit b3b01bc)
86 |
87 | # Installation
88 |
89 | ## Manual installation
90 | - Make sure you have Python 3.7+ installed. (I developed it with Python 3.8.1)
91 | - Some Python modules are required which are contained in `requirements.txt` and will be installed below.
92 |
93 | ### 1. Chromedriver & Google Chrome
94 | This project uses Selenium and automatically downloads the correct driver for your Chrome version. \
95 | ⚠️ So just make sure to have Google Chrome installed.
96 |
97 | ### 2. Cloning
98 | Open your terminal, and execute the following commands :
99 | ```bash
100 | git clone https://github.com/mxrch/ghunt
101 | cd ghunt
102 | ```
103 |
104 | ### 3. Requirements
105 | In the GHunt folder, run:
106 | ```bash
107 | python3 -m pip install -r requirements.txt
108 | ```
109 | Adapt the command to your operating system if needed.
110 |
111 | ## Docker
112 | The Docker image is automatically built and pushed to Dockerhub after each push on this repo.\
113 | You can pull the Docker image with:
114 |
115 | ```
116 | docker pull ghcr.io/mxrch/ghunt
117 | ```
118 |
119 | Then, you can use the `docker_check_and_gen.sh` and `docker_hunt.sh` to invoke GHunt through Docker, or you can use these commants :
120 |
121 | ```
122 | docker run -v ghunt-resources:/usr/src/app/resources -ti ghcr.io/mxrch/ghunt check_and_gen.py
123 | docker run -v ghunt-resources:/usr/src/app/resources -ti ghcr.io/mxrch/ghunt ghunt.py
124 | ```
125 |
126 | # Usage
127 | For the first run and sometime after, you'll need to check the validity of your cookies.\
128 | To do this, run `check_and_gen.py`. \
129 | If you don't have cookies stored (ex: first launch), you will be asked for the required cookies. If they are valid, it will generate the Authentication token and the Google Docs & Hangouts tokens.
130 |
131 | Then, you can run the tool like this:
132 | ```bash
133 | python3 ghunt.py email larry@google.com
134 | ```
135 | ```bash
136 | python3 ghunt.py doc https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms
137 | ```
138 |
139 | ⚠️ I suggest you make an empty account just for this or use an account where you never login because depending on your browser/location, re-logging in into the Google Account used for the cookies can deauthorize them.
140 |
141 | # Where I get these cookies ?
142 |
143 | ## Auto (faster)
144 | You can download the GHunt Companion extension that will automate the cookies extraction in 1-click !\
145 | \
146 | [](https://addons.mozilla.org/fr/firefox/addon/ghunt-companion/) [](https://chrome.google.com/webstore/detail/ghunt-companion/dpdcofblfbmmnikcbmmiakkclocadjab) [](https://microsoftedge.microsoft.com/addons/detail/ghunt-companion/jhgmpcigklnbjglpipnbnjhdncoihhdj)
147 |
148 | You just need to launch the check_and_gen.py file and choose the extraction mode you want to use, between putting GHunt in listening mode, or copy/paste the encoded cookies in base64.
149 |
150 | ## Manual
151 | 1. Be logged-in to myaccount.google.com
152 | 2. After that, open the Dev Tools window and navigate to the Network tab\
153 | If you don't know how to open it, just right-click anywhere and click "Inspect Element".
154 | 3. Go to myaccount.google.com, and in the browser requests, select the GET on "accounts.google.com" that gives a 302 redirect
155 | 4. Then you'll find every cookie you need in the "cookies" section.
156 |
157 | 
158 |
159 | # 🛡️ Protecting yourself
160 | Regarding the collection of metadata from your Google Photos account:
161 |
162 | Given that Google shows **"X require access"** on [your Google Account Dashboard](https://myaccount.google.com/intro/dashboard), you might imagine that you had to explicitly authorize another account in order for it to access your pictures; but this is not the case.\
163 | Any account can access your AlbumArchive (by default):
164 |
165 | 
166 |
167 | Here's how to check and fix the fact that you're vulnerable (which you most likely are):\
168 | Go to https://get.google.com/albumarchive/ while logged in with your Google account. You will be **automatically** redirected to your correct albumarchive URL (`https://get.google.com/albumarchive/YOUR-GOOGLE-ID-HERE`). After that, click the three dots on the top left corner, and click on **setting**
169 |
170 | 
171 |
172 | Then, uncheck the only option there:
173 |
174 | 
175 |
176 |
177 | On another note, the target account will **also** be vulnerable if they have ever used **Picasa** linked to their Google account in any way, shape or form. For more details on this, read PinkDev1's comment on [issue #10](https://github.com/mxrch/GHunt/issues/10).\
178 | For now, the only (known) solution to this is to delete the Picasa albums from your AlbumArchive.
179 |
180 | # Thanks
181 | This tool is based on [Sector's research on Google IDs](https://sector035.nl/articles/getting-a-grasp-on-google-ids) and completed by my own as well.\
182 | If I have the motivation to write a blog post about it, I'll add the link here !
183 | - Palenath (for the name bypass)
184 |
--------------------------------------------------------------------------------
/check_and_gen.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from lib import modwall; modwall.check() # We check the requirements
4 |
5 | import json
6 | from time import time
7 | from os.path import isfile
8 | from pathlib import Path
9 | from ssl import SSLError
10 | import base64
11 | from copy import deepcopy
12 |
13 | import httpx
14 | from seleniumwire import webdriver
15 | from selenium.common.exceptions import TimeoutException as SE_TimeoutExepction
16 | from bs4 import BeautifulSoup as bs
17 |
18 | import config
19 | from lib.utils import *
20 | from lib import listener
21 |
22 |
23 | # We change the current working directory to allow using GHunt from anywhere
24 | os.chdir(Path(__file__).parents[0])
25 |
26 | def get_saved_cookies():
27 | ''' returns cookie cache if exists '''
28 | if isfile(config.data_path):
29 | try:
30 | with open(config.data_path, 'r') as f:
31 | out = json.loads(f.read())
32 | cookies = out["cookies"]
33 | print("[+] Detected stored cookies, checking it")
34 | return cookies
35 | except Exception:
36 | print("[-] Stored cookies are corrupted\n")
37 | return False
38 | print("[-] No stored cookies found\n")
39 | return False
40 |
41 |
42 | def get_authorization_source(cookies):
43 | ''' returns html source of hangouts page if user authorized '''
44 | req = httpx.get("https://docs.google.com/document/u/0/?usp=direct_url",
45 | cookies=cookies, headers=config.headers)
46 |
47 | if req.status_code == 200:
48 | req2 = httpx.get("https://hangouts.google.com", cookies=cookies,
49 | headers=config.headers)
50 | if "myaccount.google.com" in req2.text:
51 | return req.text
52 | return None
53 |
54 |
55 | def save_tokens(hangouts_auth, gdoc_token, hangouts_token, internal_token, internal_auth, cac_key, cookies, osid):
56 | ''' save tokens to file '''
57 | output = {
58 | "hangouts_auth": hangouts_auth, "internal_auth": internal_auth,
59 | "keys": {"gdoc": gdoc_token, "hangouts": hangouts_token, "internal": internal_token, "clientauthconfig": cac_key},
60 | "cookies": cookies,
61 | "osids": {
62 | "cloudconsole": osid
63 | }
64 | }
65 | with open(config.data_path, 'w') as f:
66 | f.write(json.dumps(output))
67 |
68 |
69 | def get_hangouts_tokens(driver, cookies, tmprinter):
70 | ''' gets auth and hangouts token '''
71 |
72 | tmprinter.out("Setting cookies...")
73 | driver.get("https://hangouts.google.com/robots.txt")
74 | for k, v in cookies.items():
75 | driver.add_cookie({'name': k, 'value': v})
76 |
77 | tmprinter.out("Fetching Hangouts homepage...")
78 | driver.get("https://hangouts.google.com")
79 |
80 | tmprinter.out("Waiting for the /v2/people/me/blockedPeople request, it "
81 | "can takes a few minutes...")
82 | try:
83 | req = driver.wait_for_request('/v2/people/me/blockedPeople', timeout=config.browser_waiting_timeout)
84 | tmprinter.out("Request found !")
85 | driver.close()
86 | tmprinter.out("")
87 | except SE_TimeoutExepction:
88 | tmprinter.out("")
89 | exit("\n[!] Selenium TimeoutException has occured. Please check your internet connection, proxies, vpns, et cetera.")
90 |
91 |
92 | hangouts_auth = req.headers["Authorization"]
93 | hangouts_token = req.url.split("key=")[1]
94 |
95 | return (hangouts_auth, hangouts_token)
96 |
97 | def drive_interceptor(request):
98 | global internal_auth, internal_token
99 |
100 | if request.url.endswith(('.woff2', '.css', '.png', '.jpeg', '.svg', '.gif')):
101 | request.abort()
102 | elif request.path != "/drive/my-drive" and "Accept" in request.headers and \
103 | any([x in request.headers["Accept"] for x in ["image", "font-woff"]]):
104 | request.abort()
105 | if "authorization" in request.headers and "_" in request.headers["authorization"] and \
106 | request.headers["authorization"]:
107 | internal_auth = request.headers["authorization"]
108 |
109 | def get_internal_tokens(driver, cookies, tmprinter):
110 | """ Extract the mysterious token used for Internal People API
111 | and some Drive requests, with the Authorization header"""
112 |
113 | global internal_auth, internal_token
114 |
115 | internal_auth = ""
116 |
117 | tmprinter.out("Setting cookies...")
118 | driver.get("https://drive.google.com/robots.txt")
119 | for k, v in cookies.items():
120 | driver.add_cookie({'name': k, 'value': v})
121 |
122 | start = time()
123 |
124 | tmprinter.out("Fetching Drive homepage...")
125 | driver.request_interceptor = drive_interceptor
126 | driver.get("https://drive.google.com/drive/my-drive")
127 |
128 | body = driver.page_source
129 | internal_token = body.split("appsitemsuggest-pa")[1].split(",")[3].strip('"')
130 |
131 | tmprinter.out(f"Waiting for the authorization header, it "
132 | "can takes a few minutes...")
133 |
134 | while True:
135 | if internal_auth and internal_token:
136 | tmprinter.clear()
137 | break
138 | elif time() - start > config.browser_waiting_timeout:
139 | tmprinter.clear()
140 | exit("[-] Timeout while fetching the Internal tokens.\nPlease increase the timeout in config.py or try again.")
141 |
142 | del driver.request_interceptor
143 |
144 | return internal_auth, internal_token
145 |
146 | def gen_osid(cookies, domain, service):
147 | req = httpx.get(f"https://accounts.google.com/ServiceLogin?service={service}&osid=1&continue=https://{domain}/&followup=https://{domain}/&authuser=0",
148 | cookies=cookies, headers=config.headers)
149 |
150 | body = bs(req.text, 'html.parser')
151 |
152 | params = {x.attrs["name"]:x.attrs["value"] for x in body.find_all("input", {"type":"hidden"})}
153 |
154 | headers = {**config.headers, **{"Content-Type": "application/x-www-form-urlencoded"}}
155 | req = httpx.post(f"https://{domain}/accounts/SetOSID", cookies=cookies, data=params, headers=headers)
156 |
157 | osid_header = [x for x in req.headers["set-cookie"].split(", ") if x.startswith("OSID")]
158 | if not osid_header:
159 | exit("[-] No OSID header detected, exiting...")
160 |
161 | osid = osid_header[0].split("OSID=")[1].split(";")[0]
162 |
163 | return osid
164 |
165 | def get_clientauthconfig_key(cookies):
166 | """ Extract the Client Auth Config API token."""
167 |
168 | req = httpx.get("https://console.cloud.google.com",
169 | cookies=cookies, headers=config.headers)
170 |
171 | if req.status_code == 200 and "pantheon_apiKey" in req.text:
172 | cac_key = req.text.split('pantheon_apiKey\\x22:')[1].split(",")[0].strip('\\x22')
173 | return cac_key
174 | exit("[-] I can't find the Client Auth Config API...")
175 |
176 | def check_cookies(cookies):
177 | wanted = ["authuser", "continue", "osidt", "ifkv"]
178 |
179 | req = httpx.get(f"https://accounts.google.com/ServiceLogin?service=cloudconsole&osid=1&continue=https://console.cloud.google.com/&followup=https://console.cloud.google.com/&authuser=0",
180 | cookies=cookies, headers=config.headers)
181 |
182 | body = bs(req.text, 'html.parser')
183 |
184 | params = [x.attrs["name"] for x in body.find_all("input", {"type":"hidden"})]
185 | for param in wanted:
186 | if param not in params:
187 | return False
188 |
189 | return True
190 |
191 | def getting_cookies(cookies):
192 | choices = ("You can facilitate configuring GHunt by using the GHunt Companion extension on Firefox, Chrome, Edge and Opera here :\n"
193 | "=> https://github.com/mxrch/ghunt_companion\n\n"
194 | "[1] (Companion) Put GHunt on listening mode (currently not compatible with docker)\n"
195 | "[2] (Companion) Paste base64-encoded cookies\n"
196 | "[3] Enter manually all cookies\n\n"
197 | "Choice => ")
198 |
199 | choice = input(choices)
200 | if choice not in ["1","2","3"]:
201 | exit("Please choose a valid choice. Exiting...")
202 |
203 | if choice == "1":
204 | received_cookies = listener.run()
205 | cookies = json.loads(base64.b64decode(received_cookies))
206 |
207 | elif choice == "2":
208 | received_cookies = input("Paste the cookies here => ")
209 | cookies = json.loads(base64.b64decode(received_cookies))
210 |
211 | elif choice == "3":
212 | for name in cookies.keys():
213 | if not cookies[name]:
214 | cookies[name] = input(f"{name} => ").strip().strip('\"')
215 |
216 | return cookies
217 |
218 | if __name__ == '__main__':
219 |
220 | driverpath = get_driverpath()
221 | cookies_from_file = get_saved_cookies()
222 |
223 | tmprinter = TMPrinter()
224 |
225 | cookies = {"SID": "", "SSID": "", "APISID": "", "SAPISID": "", "HSID": "", "LSID": "", "__Secure-3PSID": "", "CONSENT": config.default_consent_cookie, "PREF": config.default_pref_cookie}
226 |
227 | new_cookies_entered = False
228 |
229 | if not cookies_from_file:
230 | cookies = getting_cookies(cookies)
231 | new_cookies_entered = True
232 | else:
233 | # in case user wants to enter new cookies (example: for new account)
234 | html = get_authorization_source(cookies_from_file)
235 | valid_cookies = check_cookies(cookies_from_file)
236 | valid = False
237 | if html and valid_cookies:
238 | print("[+] The cookies seems valid !")
239 | valid = True
240 | else:
241 | print("[-] Seems like the cookies are invalid.")
242 | new_gen_inp = input("\nDo you want to enter new browser cookies from accounts.google.com ? (Y/n) ").lower()
243 | if new_gen_inp == "y":
244 | cookies = getting_cookies(cookies)
245 | new_cookies_entered = True
246 |
247 | elif not valid:
248 | exit("Please put valid cookies. Exiting...")
249 |
250 |
251 | # Validate cookies
252 | if new_cookies_entered or not cookies_from_file:
253 | html = get_authorization_source(cookies)
254 | if html:
255 | print("\n[+] The cookies seems valid !")
256 | else:
257 | exit("\n[-] Seems like the cookies are invalid, try regenerating them.")
258 |
259 | if not new_cookies_entered:
260 | cookies = cookies_from_file
261 | choice = input("Do you want to generate new tokens ? (Y/n) ").lower()
262 | if choice != "y":
263 | exit()
264 |
265 | # Start the extraction process
266 |
267 | # We first initialize the browser driver
268 | chrome_options = get_chrome_options_args(config.headless)
269 | options = {
270 | 'connection_timeout': None # Never timeout, otherwise it floods errors
271 | }
272 |
273 | tmprinter.out("Starting browser...")
274 | driver = webdriver.Chrome(
275 | executable_path=driverpath, seleniumwire_options=options,
276 | options=chrome_options
277 | )
278 | driver.header_overrides = config.headers
279 |
280 | print("Extracting the tokens...\n")
281 | # Extracting Google Docs token
282 | trigger = '\"token\":\"'
283 | if trigger not in html:
284 | exit("[-] I can't find the Google Docs token in the source code...\n")
285 | else:
286 | gdoc_token = html.split(trigger)[1][:100].split('"')[0]
287 | print("Google Docs Token => {}".format(gdoc_token))
288 |
289 | print("Generating OSID for the Cloud Console...")
290 | osid = gen_osid(cookies, "console.cloud.google.com", "cloudconsole")
291 | cookies_with_osid = deepcopy(cookies)
292 | cookies_with_osid["OSID"] = osid
293 | # Extracting Internal People API tokens
294 | internal_auth, internal_token = get_internal_tokens(driver, cookies_with_osid, tmprinter)
295 | print(f"Internal APIs Token => {internal_token}")
296 | print(f"Internal APIs Authorization => {internal_auth}")
297 |
298 | # Extracting Hangouts tokens
299 | auth_token, hangouts_token = get_hangouts_tokens(driver, cookies_with_osid, tmprinter)
300 | print(f"Hangouts Authorization => {auth_token}")
301 | print(f"Hangouts Token => {hangouts_token}")
302 |
303 | cac_key = get_clientauthconfig_key(cookies_with_osid)
304 | print(f"Client Auth Config API Key => {cac_key}")
305 |
306 | save_tokens(auth_token, gdoc_token, hangouts_token, internal_token, internal_auth, cac_key, cookies, osid)
307 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | regexs = {
2 | "albums": r'href=\"\.\/albumarchive\/\d*?\/album\/(.*?)\" jsaction.*?>(?:<.*?>){5}(.*?)<\/div><.*?>(\d*?) ',
3 | "photos": r'\],\"(https:\/\/lh\d\.googleusercontent\.com\/.*?)\",\[\"\d{21}\"(?:.*?,){16}\"(.*?)\"',
4 | "review_loc_by_id": r'{}\",.*?\[\[null,null,(.*?),(.*?)\]',
5 | "gplus": r"plus\.google\.com\/\d*\""
6 | }
7 |
8 | headers = {
9 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0',
10 | 'Connection': 'Keep-Alive'
11 | }
12 |
13 | headless = True # if True, it doesn't show the browser while scraping GMaps reviews
14 | ytb_hunt_always = True # if True, search the Youtube channel everytime
15 | gmaps_radius = 30 # in km. The radius distance to create groups of gmaps reviews.
16 | gdocs_public_doc = "1jaEEHZL32t1RUN5WuZEnFpqiEPf_APYKrRBG9LhLdvE" # The public Google Doc to use it as an endpoint, to use Google's Search.
17 | data_path = "resources/data.txt"
18 | browser_waiting_timeout = 120
19 |
20 | # Profile pictures options
21 | write_profile_pic = True
22 | profile_pics_dir = "profile_pics"
23 |
24 | # Cookies
25 | # if True, it will uses the Google Account cookies to request the services,
26 | # and gonna be able to read your personal informations
27 | gmaps_cookies = False
28 | calendar_cookies = False
29 | default_consent_cookie = "YES+FR.fr+V10+BX"
30 | default_pref_cookie = "tz=Europe.Paris&f6=40000000&hl=en" # To set the lang settings to english
--------------------------------------------------------------------------------
/docker/download_chromedriver.py:
--------------------------------------------------------------------------------
1 | from webdriver_manager.chrome import ChromeDriverManager
2 |
3 | ChromeDriverManager(path="/usr/src/app").install()
4 | print('ChromeDriver download was successful.')
5 |
--------------------------------------------------------------------------------
/docker_check_and_gen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | docker run -v ghunt-resources:/usr/src/app/resources -ti ghcr.io/mxrch/ghunt check_and_gen.py
3 |
--------------------------------------------------------------------------------
/docker_hunt.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | docker run -v ghunt-resources:/usr/src/app/resources -ti ghcr.io/mxrch/ghunt ghunt.py $1 $2
--------------------------------------------------------------------------------
/ghunt.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from lib import modwall; modwall.check() # We check the requirements
4 |
5 | import sys
6 | import os
7 | from pathlib import Path
8 |
9 | from lib import modwall
10 | from lib.utils import *
11 | from modules.doc import doc_hunt
12 | from modules.email import email_hunt
13 | from modules.gaia import gaia_hunt
14 | from modules.youtube import youtube_hunt
15 |
16 |
17 | if __name__ == "__main__":
18 |
19 | # We change the current working directory to allow using GHunt from anywhere
20 | os.chdir(Path(__file__).parents[0])
21 |
22 | modules = ["email", "doc", "gaia", "youtube"]
23 |
24 | if len(sys.argv) <= 1 or sys.argv[1].lower() not in modules:
25 | print("Please choose a module.\n")
26 | print("Available modules :")
27 | for module in modules:
28 | print(f"- {module}")
29 | exit()
30 |
31 | module = sys.argv[1].lower()
32 | if len(sys.argv) >= 3:
33 | data = sys.argv[2]
34 | else:
35 | data = None
36 |
37 | if module == "email":
38 | email_hunt(data)
39 | elif module == "doc":
40 | doc_hunt(data)
41 | elif module == "gaia":
42 | gaia_hunt(data)
43 | elif module == "youtube":
44 | youtube_hunt(data)
--------------------------------------------------------------------------------
/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soxoj/GHunt/b41322364294850678f4b80f6a5cf0ee36a5dc54/lib/__init__.py
--------------------------------------------------------------------------------
/lib/banner.py:
--------------------------------------------------------------------------------
1 | from colorama import init, Fore, Back, Style
2 |
3 | def banner():
4 | init()
5 |
6 | banner = """
7 | """ + Fore.RED + """ .d8888b. """ + Fore.BLUE + """888 888""" + Fore.RED + """ 888
8 | """ + Fore.RED + """d88P Y88b """ + Fore.BLUE + """888 888""" + Fore.RED + """ 888
9 | """ + Fore.YELLOW + """888 """ + Fore.RED + """888 """ + Fore.BLUE + """888 888""" + Fore.RED + """ 888
10 | """ + Fore.YELLOW + """888 """ + Fore.BLUE + """8888888888""" + Fore.GREEN + """ 888 888""" + Fore.YELLOW + """ 88888b. """ + Fore.RED + """ 888888
11 | """ + Fore.YELLOW + """888 """ + Fore.BLUE + """88888 """ + Fore.BLUE + """888 888""" + Fore.GREEN + """ 888 888""" + Fore.YELLOW + """ 888 "88b""" + Fore.RED + """ 888
12 | """ + Fore.YELLOW + """888 """ + Fore.BLUE + """888 """ + Fore.BLUE + """888 888""" + Fore.GREEN + """ 888 888""" + Fore.YELLOW + """ 888 888""" + Fore.RED + """ 888
13 | """ + Fore.GREEN + """Y88b d88P """ + Fore.BLUE + """888 888""" + Fore.GREEN + """ Y88b 888""" + Fore.YELLOW + """ 888 888""" + Fore.RED + """ Y88b.
14 | """ + Fore.GREEN + """ "Y8888P88 """ + Fore.BLUE + """888 888""" + Fore.GREEN + """ "Y88888""" + Fore.YELLOW + """ 888 888""" + Fore.RED + """ "Y888
15 | """ + Fore.RESET
16 |
17 | print(banner)
18 |
19 |
--------------------------------------------------------------------------------
/lib/calendar.py:
--------------------------------------------------------------------------------
1 | import httpx
2 | from dateutil.relativedelta import relativedelta
3 | from beautifultable import BeautifulTable
4 | from termcolor import colored
5 |
6 | import time
7 | import json
8 | from datetime import datetime, timezone
9 | from urllib.parse import urlencode
10 |
11 |
12 | # assembling the json request url endpoint
13 | def assemble_api_req(calendarId, singleEvents, maxAttendees, maxResults, sanitizeHtml, timeMin, API_key, email):
14 | base_url = f"https://clients6.google.com/calendar/v3/calendars/{email}/events?"
15 | params = {
16 | "calendarId": calendarId,
17 | "singleEvents": singleEvents,
18 | "maxAttendees": maxAttendees,
19 | "maxResults": maxResults,
20 | "timeMin": timeMin,
21 | "key": API_key
22 | }
23 | base_url += urlencode(params, doseq=True)
24 | return base_url
25 |
26 | # from iso to datetime object in utc
27 | def get_datetime_utc(date_str):
28 | date = datetime.fromisoformat(date_str)
29 | margin = date.utcoffset()
30 | return date.replace(tzinfo=timezone.utc) - margin
31 |
32 | # main method of calendar.py
33 | def fetch(email, client, config):
34 | if not config.calendar_cookies:
35 | cookies = {"CONSENT": config.default_consent_cookie}
36 | client.cookies = cookies
37 | url_endpoint = f"https://calendar.google.com/calendar/u/0/embed?src={email}"
38 | print("\nGoogle Calendar : " + url_endpoint)
39 | req = client.get(url_endpoint + "&hl=en")
40 | source = req.text
41 | try:
42 | # parsing parameters from source code
43 | calendarId = source.split('title\":\"')[1].split('\"')[0]
44 | singleEvents = "true"
45 | maxAttendees = 1
46 | maxResults = 250
47 | sanitizeHtml = "true"
48 | timeMin = datetime.strptime(source.split('preloadStart\":\"')[1].split('\"')[0], '%Y%m%d').replace(tzinfo=timezone.utc).isoformat()
49 | API_key = source.split('developerKey\":\"')[1].split('\"')[0]
50 | except IndexError:
51 | return False
52 |
53 | json_calendar_endpoint = assemble_api_req(calendarId, singleEvents, maxAttendees, maxResults, sanitizeHtml, timeMin, API_key, email)
54 | req = client.get(json_calendar_endpoint)
55 | data = json.loads(req.text)
56 | events = []
57 | try:
58 | for item in data["items"]:
59 | title = item["summary"]
60 | start = get_datetime_utc(item["start"]["dateTime"])
61 | end = get_datetime_utc(item["end"]["dateTime"])
62 |
63 | events.append({"title": title, "start": start, "end": end})
64 | except KeyError:
65 | return False
66 |
67 | return {"status": "available", "events": events}
68 |
69 | def out(events):
70 | limit = 5
71 | now = datetime.utcnow().replace(tzinfo=timezone.utc)
72 | after = [date for date in events if date["start"] >= now][:limit]
73 | before = [date for date in events if date["start"] <= now][:limit]
74 | print(f"\n=> The {'next' if after else 'last'} {len(after) if after else len(before)} event{'s' if (len(after) > 1) or (not after and len(before) > 1) else ''} :")
75 | target = after if after else before
76 |
77 | table = BeautifulTable()
78 | table.set_style(BeautifulTable.STYLE_GRID)
79 | table.columns.header = [colored(x, attrs=['bold']) for x in ["Name", "Datetime (UTC)", "Duration"]]
80 | for event in target:
81 | title = event["title"]
82 | duration = relativedelta(event["end"], event["start"])
83 | if duration.days or duration.hours or duration.minutes:
84 | duration = (f"{(str(duration.days) + ' day' + ('s' if duration.days > 1 else '')) if duration.days else ''} "
85 | f"{(str(duration.hours) + ' hour' + ('s' if duration.hours > 1 else '')) if duration.hours else ''} "
86 | f"{(str(duration.minutes) + ' minute' + ('s' if duration.minutes > 1 else '')) if duration.minutes else ''}").strip()
87 | else:
88 | duration = "?"
89 | date = event["start"].strftime("%Y/%m/%d %H:%M:%S")
90 | table.rows.append([title, date, duration])
91 | print(table)
--------------------------------------------------------------------------------
/lib/gmaps.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import re
3 | import time
4 | from datetime import datetime
5 |
6 | from dateutil.relativedelta import relativedelta
7 | from geopy import distance
8 | from geopy.geocoders import Nominatim
9 | from selenium.webdriver.common.by import By
10 | from selenium.webdriver.support import expected_conditions as EC
11 | from selenium.webdriver.support.ui import WebDriverWait
12 | from seleniumwire import webdriver
13 | from webdriver_manager.chrome import ChromeDriverManager
14 |
15 | from lib.utils import *
16 |
17 |
18 | def scrape(gaiaID, client, cookies, config, headers, regex_rev_by_id, is_headless):
19 | def get_datetime(datepublished):
20 | if datepublished.split()[0] == "a":
21 | nb = 1
22 | else:
23 | nb = int(datepublished.split()[0])
24 | if "minute" in datepublished:
25 | delta = relativedelta(minutes=nb)
26 | elif "hour" in datepublished:
27 | delta = relativedelta(hours=nb)
28 | elif "day" in datepublished:
29 | delta = relativedelta(days=nb)
30 | elif "week" in datepublished:
31 | delta = relativedelta(weeks=nb)
32 | elif "month" in datepublished:
33 | delta = relativedelta(months=nb)
34 | elif "year" in datepublished:
35 | delta = relativedelta(years=nb)
36 | else:
37 | delta = relativedelta()
38 | return (datetime.today() - delta).replace(microsecond=0, second=0)
39 |
40 | tmprinter = TMPrinter()
41 |
42 | base_url = f"https://www.google.com/maps/contrib/{gaiaID}/reviews?hl=en"
43 | print(f"\nGoogle Maps : {base_url.replace('?hl=en', '')}")
44 |
45 | tmprinter.out("Initial request...")
46 |
47 | req = client.get(base_url)
48 | source = req.text
49 |
50 | data = source.split(';window.APP_INITIALIZATION_STATE=')[1].split(';window.APP_FLAGS')[0].replace("\\", "")
51 |
52 | if "/maps/reviews/data" not in data:
53 | tmprinter.out("")
54 | print("[-] No reviews")
55 | return False
56 |
57 | chrome_options = get_chrome_options_args(is_headless)
58 | options = {
59 | 'connection_timeout': None # Never timeout, otherwise it floods errors
60 | }
61 |
62 | tmprinter.out("Starting browser...")
63 |
64 | driverpath = get_driverpath()
65 | driver = webdriver.Chrome(executable_path=driverpath, seleniumwire_options=options, options=chrome_options)
66 | driver.header_overrides = headers
67 | wait = WebDriverWait(driver, 15)
68 |
69 | tmprinter.out("Setting cookies...")
70 | driver.get("https://www.google.com/robots.txt")
71 |
72 | if not config.gmaps_cookies:
73 | cookies = {"CONSENT": config.default_consent_cookie}
74 | for k, v in cookies.items():
75 | driver.add_cookie({'name': k, 'value': v})
76 |
77 | tmprinter.out("Fetching reviews page...")
78 | driver.get(base_url)
79 |
80 | wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'div.section-scrollbox')))
81 | scrollbox = driver.find_element(By.CSS_SELECTOR, 'div.section-scrollbox')
82 |
83 | tab_info = scrollbox.find_element(By.TAG_NAME, "div")
84 | if tab_info and tab_info.text:
85 | scroll_max = sum([int(x) for x in tab_info.text.split() if x.isdigit()])
86 | else:
87 | return False
88 |
89 | tmprinter.clear()
90 | print(f"[+] {scroll_max} reviews found !")
91 |
92 | timeout = scroll_max * 1.25
93 | timeout_start = time.time()
94 | reviews_elements = driver.find_elements_by_xpath('//div[@data-review-id][@aria-label]')
95 | tmprinter.out(f"Fetching reviews... ({len(reviews_elements)}/{scroll_max})")
96 | while len(reviews_elements) < scroll_max:
97 | driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", scrollbox)
98 | reviews_elements = driver.find_elements_by_xpath('//div[@data-review-id][@aria-label]')
99 | tmprinter.out(f"Fetching reviews... ({len(reviews_elements)}/{scroll_max})")
100 | if time.time() > timeout_start + timeout:
101 | tmprinter.out(f"Timeout while fetching reviews !")
102 | break
103 |
104 | tmprinter.out("Fetching internal requests history...")
105 | requests = [r.url for r in driver.requests if "locationhistory" in r.url]
106 | tmprinter.out(f"Fetching internal requests... (0/{len(requests)})")
107 | for nb, load in enumerate(requests):
108 | req = client.get(load)
109 | data += req.text.replace('\n', '')
110 | tmprinter.out(f"Fetching internal requests... ({nb + 1}/{len(requests)})")
111 |
112 | tmprinter.out(f"Fetching reviews location... (0/{len(reviews_elements)})")
113 | reviews = []
114 | rating = 0
115 | for nb, review in enumerate(reviews_elements):
116 | id = review.get_attribute("data-review-id")
117 | location = re.compile(regex_rev_by_id.format(id)).findall(data)[0]
118 | try:
119 | stars = review.find_element(By.CSS_SELECTOR, 'span[aria-label$="stars "]')
120 | except Exception:
121 | stars = review.find_element(By.CSS_SELECTOR, 'span[aria-label$="star "]')
122 | rating += int(stars.get_attribute("aria-label").strip().split()[0])
123 | date = get_datetime(stars.find_element(By.XPATH, "following-sibling::span").text)
124 | reviews.append({"location": location, "date": date})
125 | tmprinter.out(f"Fetching reviews location... ({nb + 1}/{len(reviews_elements)})")
126 |
127 | rating_avg = rating / len(reviews)
128 | tmprinter.clear()
129 | print(f"[+] Average rating : {int(rating_avg) if int(rating_avg) / round(rating_avg, 1) == 1 else round(rating_avg, 1)}/5 stars !")
130 | # 4.9 => 4.9, 5.0 => 5, we don't show the 0
131 | return reviews
132 |
133 |
134 | def avg_location(locs):
135 | latitude = []
136 | longitude = []
137 | for loc in locs:
138 | latitude.append(float(loc[0]))
139 | longitude.append(float(loc[1]))
140 |
141 | latitude = sum(latitude) / len(latitude)
142 | longitude = sum(longitude) / len(longitude)
143 | return latitude, longitude
144 |
145 |
146 | def translate_confidence(percents):
147 | if percents >= 100:
148 | return "Extremely high"
149 | elif percents >= 80:
150 | return "Very high"
151 | elif percents >= 60:
152 | return "Little high"
153 | elif percents >= 40:
154 | return "Okay"
155 | elif percents >= 20:
156 | return "Low"
157 | elif percents >= 10:
158 | return "Very low"
159 | else:
160 | return "Extremely low"
161 |
162 |
163 | def get_confidence(geolocator, data, gmaps_radius):
164 | tmprinter = TMPrinter()
165 | radius = gmaps_radius
166 |
167 | locations = {}
168 | tmprinter.out(f"Calculation of the distance of each review...")
169 | for nb, review in enumerate(data):
170 | hash = hashlib.md5(str(review).encode()).hexdigest()
171 | if hash not in locations:
172 | locations[hash] = {"dates": [], "locations": [], "range": None, "score": 0}
173 | location = review["location"]
174 | for review2 in data:
175 | location2 = review2["location"]
176 | dis = distance.distance(location, location2).km
177 |
178 | if dis <= radius:
179 | locations[hash]["dates"].append(review2["date"])
180 | locations[hash]["locations"].append(review2["location"])
181 |
182 | maxdate = max(locations[hash]["dates"])
183 | mindate = min(locations[hash]["dates"])
184 | locations[hash]["range"] = maxdate - mindate
185 | tmprinter.out(f"Calculation of the distance of each review ({nb}/{len(data)})...")
186 |
187 | tmprinter.out("")
188 |
189 | locations = {k: v for k, v in
190 | sorted(locations.items(), key=lambda k: len(k[1]["locations"]), reverse=True)} # We sort it
191 |
192 | tmprinter.out("Identification of redundant areas...")
193 | to_del = []
194 | for hash in locations:
195 | if hash in to_del:
196 | continue
197 | for hash2 in locations:
198 | if hash2 in to_del or hash == hash2:
199 | continue
200 | if all([loc in locations[hash]["locations"] for loc in locations[hash2]["locations"]]):
201 | to_del.append(hash2)
202 | for hash in to_del:
203 | del locations[hash]
204 |
205 | tmprinter.out("Calculating confidence...")
206 | maxrange = max([locations[hash]["range"] for hash in locations])
207 | maxlen = max([len(locations[hash]["locations"]) for hash in locations])
208 | minreq = 3
209 | mingroups = 3
210 |
211 | score_steps = 4
212 | for hash, loc in locations.items():
213 | if len(loc["locations"]) == maxlen:
214 | locations[hash]["score"] += score_steps * 4
215 | if loc["range"] == maxrange:
216 | locations[hash]["score"] += score_steps * 3
217 | if len(locations) >= mingroups:
218 | others = sum([len(locations[h]["locations"]) for h in locations if h != hash])
219 | if len(loc["locations"]) > others:
220 | locations[hash]["score"] += score_steps * 2
221 | if len(loc["locations"]) >= minreq:
222 | locations[hash]["score"] += score_steps
223 |
224 | # for hash,loc in locations.items():
225 | # print(f"{hash} => {len(loc['locations'])} ({int(loc['score'])/40*100})")
226 |
227 | panels = sorted(set([loc["score"] for loc in locations.values()]), reverse=True)
228 |
229 | maxscore = sum([p * score_steps for p in range(1, score_steps + 1)])
230 | for panel in panels:
231 | locs = [loc for loc in locations.values() if loc["score"] == panel]
232 | if len(locs[0]["locations"]) == 1:
233 | panel /= 2
234 | if len(data) < 4:
235 | panel /= 2
236 | confidence = translate_confidence(panel / maxscore * 100)
237 | for nb, loc in enumerate(locs):
238 | avg = avg_location(loc["locations"])
239 | #import pdb; pdb.set_trace()
240 | while True:
241 | try:
242 | location = geolocator.reverse(f"{avg[0]}, {avg[1]}", timeout=10).raw["address"]
243 | break
244 | except:
245 | pass
246 | location = sanitize_location(location)
247 | locs[nb]["avg"] = location
248 | del locs[nb]["locations"]
249 | del locs[nb]["score"]
250 | del locs[nb]["range"]
251 | del locs[nb]["dates"]
252 | tmprinter.out("")
253 | return confidence, locs
254 |
--------------------------------------------------------------------------------
/lib/listener.py:
--------------------------------------------------------------------------------
1 | from http.server import BaseHTTPRequestHandler, HTTPServer
2 | import threading
3 |
4 | from time import sleep
5 |
6 | class DataBridge():
7 | def __init__(self):
8 | self.data = None
9 |
10 | class Server(BaseHTTPRequestHandler):
11 | def _set_response(self):
12 | self.send_response(200)
13 | self.send_header('Content-type', 'text/html')
14 | self.send_header('Access-Control-Allow-Origin','*')
15 | self.end_headers()
16 |
17 | def do_GET(self):
18 | if self.path == "/ghunt_ping":
19 | self._set_response()
20 | self.wfile.write(b"ghunt_pong")
21 |
22 | def do_POST(self):
23 | if self.path == "/ghunt_feed":
24 | content_length = int(self.headers['Content-Length']) # <--- Gets the size of data
25 | post_data = self.rfile.read(content_length) # <--- Gets the data itself
26 | self.data_bridge.data = post_data.decode('utf-8')
27 |
28 | self._set_response()
29 | self.wfile.write(b"ghunt_received_ok")
30 |
31 | def log_message(self, format, *args):
32 | return
33 |
34 | def run(server_class=HTTPServer, handler_class=Server, port=60067):
35 | server_address = ('127.0.0.1', port)
36 | handler_class.data_bridge = DataBridge()
37 | server = server_class(server_address, handler_class)
38 | try:
39 | print(f"GHunt is listening on port {port}...")
40 |
41 | while True:
42 | server.handle_request()
43 | if handler_class.data_bridge.data:
44 | break
45 |
46 | except KeyboardInterrupt:
47 | exit("[-] Exiting...")
48 | else:
49 | if handler_class.data_bridge.data:
50 | print("[+] Received cookies !")
51 | return handler_class.data_bridge.data
--------------------------------------------------------------------------------
/lib/metadata.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from PIL import ExifTags
4 | from PIL.ExifTags import TAGS, GPSTAGS
5 | from geopy.geocoders import Nominatim
6 |
7 | from lib.utils import *
8 |
9 |
10 | class ExifEater():
11 |
12 | def __init__(self):
13 | self.devices = {}
14 | self.softwares = {}
15 | self.locations = {}
16 | self.geolocator = Nominatim(user_agent="nominatim")
17 |
18 | def get_GPS(self, img):
19 | location = ""
20 | geoaxis = {}
21 | geotags = {}
22 | try:
23 | exif = img._getexif()
24 |
25 | for (idx, tag) in TAGS.items():
26 | if tag == 'GPSInfo':
27 | if idx in exif:
28 | for (key, val) in GPSTAGS.items():
29 | if key in exif[idx]:
30 | geotags[val] = exif[idx][key]
31 |
32 | for axis in ["Latitude", "Longitude"]:
33 | dms = geotags[f'GPS{axis}']
34 | ref = geotags[f'GPS{axis}Ref']
35 |
36 | degrees = dms[0][0] / dms[0][1]
37 | minutes = dms[1][0] / dms[1][1] / 60.0
38 | seconds = dms[2][0] / dms[2][1] / 3600.0
39 |
40 | if ref in ['S', 'W']:
41 | degrees = -degrees
42 | minutes = -minutes
43 | seconds = -seconds
44 |
45 | geoaxis[axis] = round(degrees + minutes + seconds, 5)
46 | location = \
47 | self.geolocator.reverse("{}, {}".format(geoaxis["Latitude"], geoaxis["Longitude"])).raw[
48 | "address"]
49 | except Exception:
50 | return ""
51 | else:
52 | if location:
53 | location = sanitize_location(location)
54 | if not location:
55 | return ""
56 | return f'{location["town"]}, {location["country"]}'
57 | else:
58 | return ""
59 |
60 | def feed(self, img):
61 | try:
62 | img._getexif()
63 | except:
64 | try:
65 | img._getexif = img.getexif
66 | except:
67 | img._getexif = lambda d={}:d
68 | if img._getexif():
69 | location = self.get_GPS(img)
70 | exif = {ExifTags.TAGS[k]: v for k, v in img._getexif().items() if k in ExifTags.TAGS}
71 | interesting_fields = ["Make", "Model", "DateTime", "Software"]
72 | metadata = {k: v for k, v in exif.items() if k in interesting_fields}
73 | try:
74 | date = datetime.strptime(metadata["DateTime"], '%Y:%m:%d %H:%M:%S')
75 | is_date_valid = "Valid"
76 | except Exception:
77 | date = None
78 | is_date_valid = "Invalid"
79 |
80 | if location:
81 | if location not in self.locations:
82 | self.locations[location] = {"Valid": [], "Invalid": []}
83 | self.locations[location][is_date_valid].append(date)
84 | if "Make" in metadata and "Model" in metadata:
85 | if metadata["Model"] not in self.devices:
86 | self.devices[metadata["Model"]] = {"Make": metadata["Make"],
87 | "History": {"Valid": [], "Invalid": []}, "Firmwares": {}}
88 | self.devices[metadata["Model"]]["History"][is_date_valid].append(date)
89 | if "Software" in metadata:
90 | if metadata["Software"] not in self.devices[metadata["Model"]]["Firmwares"]:
91 | self.devices[metadata["Model"]]["Firmwares"][metadata["Software"]] = {"Valid": [],
92 | "Invalid": []}
93 | self.devices[metadata["Model"]]["Firmwares"][metadata["Software"]][is_date_valid].append(date)
94 | elif "Software" in metadata:
95 | if metadata["Software"] not in self.softwares:
96 | self.softwares[metadata["Software"]] = {"Valid": [], "Invalid": []}
97 | self.softwares[metadata["Software"]][is_date_valid].append(date)
98 |
99 | def give_back(self):
100 | return self.locations, self.devices
101 |
102 | def output(self):
103 | bkn = '\n' # to use in f-strings
104 |
105 | def picx(n):
106 | return "s" if n > 1 else ""
107 |
108 | def print_dates(dates_list):
109 | dates = {}
110 | dates["max"] = max(dates_list).strftime("%Y/%m/%d")
111 | dates["min"] = min(dates_list).strftime("%Y/%m/%d")
112 | if dates["max"] == dates["min"]:
113 | return dates["max"]
114 | else:
115 | return f'{dates["min"]} -> {dates["max"]}'
116 |
117 | # pprint((self.devices, self.softwares, self.locations))
118 |
119 | devices = self.devices
120 | if devices:
121 | print(f"[+] {len(devices)} device{picx(len(devices))} found !")
122 | for model, data in devices.items():
123 | make = data["Make"]
124 | if model.lower().startswith(make.lower()):
125 | model = model[len(make):].strip()
126 | n = len(data["History"]["Valid"] + data["History"]["Invalid"])
127 | for validity, dateslist in data["History"].items():
128 | if dateslist and (
129 | (validity == "Valid") or (validity == "Invalid" and not data["History"]["Valid"])):
130 | if validity == "Valid":
131 | dates = print_dates(data["History"]["Valid"])
132 | elif validity == "Valid" and data["History"]["Invalid"]:
133 | dates = print_dates(data["History"]["Valid"])
134 | dates += " (+ ?)"
135 | elif validity == "Invalid" and not data["History"]["Valid"]:
136 | dates = "?"
137 | print(
138 | f"{bkn if data['Firmwares'] else ''}- {make.capitalize()} {model} ({n} pic{picx(n)}) [{dates}]")
139 | if data["Firmwares"]:
140 | n = len(data['Firmwares'])
141 | print(f"-> {n} Firmware{picx(n)} found !")
142 | for firmware, firmdata in data["Firmwares"].items():
143 | for validity2, dateslist2 in firmdata.items():
144 | if dateslist2 and ((validity2 == "Valid") or (
145 | validity2 == "Invalid" and not firmdata["Valid"])):
146 | if validity2 == "Valid":
147 | dates2 = print_dates(firmdata["Valid"])
148 | elif validity2 == "Valid" and firmdata["Invalid"]:
149 | dates2 = print_dates(firmdata["Valid"])
150 | dates2 += " (+ ?)"
151 | elif validity2 == "Invalid" and not firmdata["Valid"]:
152 | dates2 = "?"
153 | print(f"--> {firmware} [{dates2}]")
154 |
155 | locations = self.locations
156 | if locations:
157 | print(f"\n[+] {len(locations)} location{picx(len(locations))} found !")
158 | for location, data in locations.items():
159 | n = len(data["Valid"] + data["Invalid"])
160 | for validity, dateslist in data.items():
161 | if dateslist and ((validity == "Valid") or (validity == "Invalid" and not data["Valid"])):
162 | if validity == "Valid":
163 | dates = print_dates(data["Valid"])
164 | elif validity == "Valid" and data["Invalid"]:
165 | dates = print_dates(data["Valid"])
166 | dates += " (+ ?)"
167 | elif validity == "Invalid" and not data["Valid"]:
168 | dates = "?"
169 | print(f"- {location} ({n} pic{picx(n)}) [{dates}]")
170 |
171 | softwares = self.softwares
172 | if softwares:
173 | print(f"\n[+] {len(softwares)} software{picx(len(softwares))} found !")
174 | for software, data in softwares.items():
175 | n = len(data["Valid"] + data["Invalid"])
176 | for validity, dateslist in data.items():
177 | if dateslist and ((validity == "Valid") or (validity == "Invalid" and not data["Valid"])):
178 | if validity == "Valid":
179 | dates = print_dates(data["Valid"])
180 | elif validity == "Valid" and data["Invalid"]:
181 | dates = print_dates(data["Valid"])
182 | dates += " (+ ?)"
183 | elif validity == "Invalid" and not data["Valid"]:
184 | dates = "?"
185 | print(f"- {software} ({n} pic{picx(n)}) [{dates}]")
186 |
187 | if not devices and not locations and not softwares:
188 | print("=> Nothing found")
189 |
--------------------------------------------------------------------------------
/lib/modwall.py:
--------------------------------------------------------------------------------
1 | from pkg_resources import parse_requirements, parse_version, working_set
2 |
3 |
4 | def print_help_and_exit():
5 | print("- Windows : py -m pip install --upgrade -r requirements.txt")
6 | print("- Unix : python3 -m pip install --upgrade -r requirements.txt")
7 | exit()
8 |
9 | def check_versions(installed_version, op, version):
10 | if (op == ">" and parse_version(installed_version) > parse_version(version)) \
11 | or (op == "<" and parse_version(installed_version) < parse_version(version)) \
12 | or (op == "==" and parse_version(installed_version) == parse_version(version)) \
13 | or (op == ">=" and parse_version(installed_version) >= parse_version(version)) \
14 | or (op == "<=" and parse_version(installed_version) <= parse_version(version)) :
15 | return True
16 | return False
17 |
18 | def check():
19 | with open('requirements.txt', "r") as requirements_raw:
20 | requirements = [{"specs": x.specs, "key": x.key} for x in parse_requirements(requirements_raw)]
21 |
22 | installed_mods = {mod.key:mod.version for mod in working_set}
23 |
24 | for req in requirements:
25 | if req["key"] not in installed_mods:
26 | print(f"[-] [modwall] I can't find the library {req['key']}, did you correctly installed the libraries specified in requirements.txt ? 😤\n")
27 | print_help_and_exit()
28 | else:
29 | if req["specs"] and (specs := req["specs"][0]):
30 | op, version = specs
31 | if not check_versions(installed_mods[req["key"]], op, version):
32 | print(f"[-] [modwall] The library {req['key']} version is {installed_mods[req['key']]} but it requires {op} {version}\n")
33 | print("Please upgrade your libraries specified in the requirements.txt file. 😇")
34 | print_help_and_exit()
--------------------------------------------------------------------------------
/lib/os_detect.py:
--------------------------------------------------------------------------------
1 | from platform import system, uname
2 |
3 |
4 | class Os:
5 | """
6 | returns class with properties:
7 | .cygwin Cygwin detected
8 | .wsl Windows Subsystem for Linux (WSL) detected
9 | .mac Mac OS detected
10 | .linux Linux detected
11 | .bsd BSD detected
12 | """
13 |
14 | def __init__(self):
15 | syst = system().lower()
16 |
17 | # initialize
18 | self.cygwin = False
19 | self.wsl = False
20 | self.mac = False
21 | self.linux = False
22 | self.windows = False
23 | self.bsd = False
24 |
25 | if 'cygwin' in syst:
26 | self.cygwin = True
27 | self.os = 'cygwin'
28 | elif 'darwin' in syst:
29 | self.mac = True
30 | self.os = 'mac'
31 | elif 'linux' in syst:
32 | self.linux = True
33 | self.os = 'linux'
34 | if 'Microsoft' in uname().release:
35 | self.wsl = True
36 | self.linux = False
37 | self.os = 'wsl'
38 | elif 'windows' in syst:
39 | self.windows = True
40 | self.os = 'windows'
41 | elif 'bsd' in syst:
42 | self.bsd = True
43 | self.os = 'bsd'
44 |
45 | def __str__(self):
46 | return self.os
47 |
--------------------------------------------------------------------------------
/lib/photos.py:
--------------------------------------------------------------------------------
1 | import re
2 | from io import BytesIO
3 | import pdb
4 |
5 | from PIL import Image
6 | from selenium.webdriver.common.by import By
7 | from selenium.webdriver.support import expected_conditions as EC
8 | from selenium.webdriver.support.ui import WebDriverWait
9 | from seleniumwire import webdriver
10 | from webdriver_manager.chrome import ChromeDriverManager
11 |
12 | from lib.metadata import ExifEater
13 | from lib.utils import *
14 |
15 |
16 | class element_has_substring_or_substring(object):
17 | def __init__(self, locator, substring1, substring2):
18 | self.locator = locator
19 | self.substring1 = substring1
20 | self.substring2 = substring2
21 |
22 | def __call__(self, driver):
23 | element = driver.find_element(*self.locator) # Finding the referenced element
24 | if self.substring1 in element.text:
25 | return self.substring1
26 | elif self.substring2 in element.text:
27 | return self.substring2
28 | else:
29 | return False
30 |
31 |
32 | def get_source(gaiaID, client, cookies, headers, is_headless):
33 | baseurl = f"https://get.google.com/albumarchive/{gaiaID}/albums/profile-photos?hl=en"
34 | req = client.get(baseurl)
35 | if req.status_code != 200:
36 | return False
37 |
38 | tmprinter = TMPrinter()
39 | chrome_options = get_chrome_options_args(is_headless)
40 | options = {
41 | 'connection_timeout': None # Never timeout, otherwise it floods errors
42 | }
43 |
44 | tmprinter.out("Starting browser...")
45 |
46 | driverpath = get_driverpath()
47 | driver = webdriver.Chrome(executable_path=driverpath, seleniumwire_options=options, options=chrome_options)
48 | driver.header_overrides = headers
49 | wait = WebDriverWait(driver, 30)
50 |
51 | tmprinter.out("Setting cookies...")
52 | driver.get("https://get.google.com/robots.txt")
53 | for k, v in cookies.items():
54 | driver.add_cookie({'name': k, 'value': v})
55 |
56 | tmprinter.out('Fetching Google Photos "Profile photos" album...')
57 | driver.get(baseurl)
58 |
59 | tmprinter.out('Fetching the Google Photos albums overview...')
60 | buttons = driver.find_elements(By.XPATH, "//button")
61 | for button in buttons:
62 | text = button.get_attribute('jsaction')
63 | if text and 'touchcancel' in text:
64 | button.click()
65 | break
66 | else:
67 | tmprinter.out("")
68 | print("Can't get the back button..")
69 | driver.close()
70 | return False
71 |
72 | wait.until(EC.text_to_be_present_in_element((By.XPATH, "//body"), "Album Archive"))
73 | tmprinter.out("Got the albums overview !")
74 | no_photos_trigger = "reached the end"
75 | photos_trigger = " item"
76 | body = driver.find_element(By.XPATH, "//body").text
77 | if no_photos_trigger in body:
78 | stats = "notfound"
79 | elif photos_trigger in body:
80 | stats = "found"
81 | else:
82 | try:
83 | result = wait.until(element_has_substring_or_substring((By.XPATH, "//body"), no_photos_trigger, photos_trigger))
84 | except Exception:
85 | tmprinter.out("[-] Timeout while fetching photos.")
86 | return False
87 | else:
88 | if result == no_photos_trigger:
89 | stats = "notfound"
90 | elif result == photos_trigger:
91 | stats = "found"
92 | else:
93 | return False
94 | tmprinter.out("")
95 | source = driver.page_source
96 | driver.close()
97 |
98 | return {"stats": stats, "source": source}
99 |
100 |
101 | def gpics(gaiaID, client, cookies, headers, regex_albums, regex_photos, headless=True):
102 | baseurl = "https://get.google.com/albumarchive/"
103 |
104 | print(f"\nGoogle Photos : {baseurl + gaiaID + '/albums/profile-photos'}")
105 | out = get_source(gaiaID, client, cookies, headers, headless)
106 |
107 | if not out:
108 | print("=> Couldn't fetch the public photos.")
109 | return False
110 | if out["stats"] == "notfound":
111 | print("=> No album")
112 | return False
113 |
114 | # open('debug.html', 'w').write(repr(out["source"]))
115 | results = re.compile(regex_albums).findall(out["source"])
116 |
117 | list_albums_length = len(results)
118 |
119 | if results:
120 | exifeater = ExifEater()
121 | pics = []
122 | for album in results:
123 | album_name = album[1]
124 | album_link = baseurl + gaiaID + "/album/" + album[0]
125 | album_length = int(album[2])
126 |
127 | if album_length >= 1:
128 | try:
129 | req = client.get(album_link)
130 | source = req.text.replace('\n', '')
131 | results_pics = re.compile(regex_photos).findall(source)
132 | for pic in results_pics:
133 | pic_name = pic[1]
134 | pic_link = pic[0]
135 | pics.append(pic_link)
136 | except:
137 | pass
138 |
139 | print(f"=> {list_albums_length} albums{', ' + str(len(pics)) + ' photos' if list_albums_length else ''}")
140 | for pic in pics:
141 | try:
142 | req = client.get(pic)
143 | img = Image.open(BytesIO(req.content))
144 | exifeater.feed(img)
145 | except:
146 | pass
147 |
148 | print("\nSearching metadata...")
149 | exifeater.output()
150 | else:
151 | print("=> No album")
152 |
--------------------------------------------------------------------------------
/lib/search.py:
--------------------------------------------------------------------------------
1 | import json
2 | import httpx
3 |
4 | from pprint import pprint
5 | from time import sleep
6 |
7 |
8 | def search(query, data_path, gdocs_public_doc, size=1000):
9 | cookies = ""
10 | token = ""
11 |
12 | with open(data_path, 'r') as f:
13 | out = json.loads(f.read())
14 | token = out["keys"]["gdoc"]
15 | cookies = out["cookies"]
16 | data = {"request": '["documentsuggest.search.search_request","{}",[{}],null,1]'.format(query, size)}
17 |
18 | retries = 10
19 | time_to_wait = 5
20 | for retry in list(range(retries))[::-1]:
21 | req = httpx.post('https://docs.google.com/document/d/{}/explore/search?token={}'.format(gdocs_public_doc, token),
22 | cookies=cookies, data=data)
23 | #print(req.text)
24 | if req.status_code == 200:
25 | break
26 | if req.status_code == 500:
27 | if retry == 0:
28 | exit(f"[-] Error (GDocs): request gives {req.status_code}, wait a minute and retry !")
29 | print(f"[-] GDocs request gives a 500 status code, retrying in 5 seconds...")
30 | continue
31 |
32 | output = json.loads(req.text.replace(")]}'", ""))
33 | if isinstance(output[0][1], str) and output[0][1].lower() == "xsrf":
34 | exit(f"\n[-] Error : XSRF detected.\nIt means your cookies have expired, please generate new ones.")
35 |
36 | results = []
37 | for result in output[0][1]:
38 | link = result[0][0]
39 | title = result[0][1]
40 | desc = result[0][2]
41 | results.append({"title": title, "desc": desc, "link": link})
42 |
43 | return results
44 |
--------------------------------------------------------------------------------
/lib/utils.py:
--------------------------------------------------------------------------------
1 | import imagehash
2 | from webdriver_manager.chrome import ChromeDriverManager
3 | from selenium.webdriver.chrome.options import Options
4 | from seleniumwire.webdriver import Chrome
5 |
6 | from lib.os_detect import Os
7 |
8 | from pathlib import Path
9 | import shutil
10 | import subprocess, os
11 | from os.path import isfile
12 | import json
13 | import re
14 | from pprint import pprint
15 |
16 |
17 | class TMPrinter():
18 | def __init__(self):
19 | self.max_len = 0
20 |
21 | def out(self, text):
22 | if len(text) > self.max_len:
23 | self.max_len = len(text)
24 | else:
25 | text += (" " * (self.max_len - len(text)))
26 | print(text, end='\r')
27 | def clear(self):
28 | print(" " * self.max_len, end="\r")
29 |
30 | def within_docker():
31 | return Path('/.dockerenv').is_file()
32 |
33 | class Picture:
34 | def __init__(self, url, is_default=False):
35 | self.url = url
36 | self.is_default = is_default
37 |
38 | class Contact:
39 | def __init__(self, val, is_primary=True):
40 | self.value = val
41 | self.is_secondary = not is_primary
42 |
43 | def is_normalized(self, val):
44 | return val.replace('.', '').lower() == self.value.replace('.', '').lower()
45 |
46 | def __str__(self):
47 | printable_value = self.value
48 | if self.is_secondary:
49 | printable_value += ' (secondary)'
50 | return printable_value
51 |
52 | def update_emails(emails, data):
53 | """
54 | Typically canonical user email
55 | May not be present in the list method response
56 | """
57 | if not "email" in data:
58 | return emails
59 |
60 | for e in data["email"]:
61 | is_primary = e.get("signupEmailMetadata", {}).get("primary")
62 | email = Contact(e["value"], is_primary)
63 |
64 | if email.value in emails:
65 | if is_primary:
66 | emails[email.value].is_secondary = False
67 | else:
68 | emails[email.value] = email
69 |
70 | return emails
71 |
72 | def is_email_google_account(httpx_client, auth, cookies, email, hangouts_token):
73 | host = "https://people-pa.clients6.google.com"
74 | url = "/v2/people/lookup?key={}".format(hangouts_token)
75 | body = """id={}&type=EMAIL&matchType=EXACT&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_LOOKUP&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&coreIdParams.useRealtimeNotificationExpandedAcls=true&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&requestMask.includeContainer=AFFINITY&requestMask.includeContainer=PROFILE&requestMask.includeContainer=DOMAIN_PROFILE&requestMask.includeContainer=ACCOUNT&requestMask.includeContainer=EXTERNAL_ACCOUNT&requestMask.includeContainer=CIRCLE&requestMask.includeContainer=DOMAIN_CONTACT&requestMask.includeContainer=DEVICE_CONTACT&requestMask.includeContainer=GOOGLE_GROUP&requestMask.includeContainer=CONTACT"""
76 |
77 | headers = {
78 | "X-HTTP-Method-Override": "GET",
79 | "Authorization": auth,
80 | "Content-Type": "application/x-www-form-urlencoded",
81 | "Origin": "https://hangouts.google.com"
82 | }
83 |
84 | req = httpx_client.post(host + url, data=body.format(email), headers=headers, cookies=cookies)
85 | data = json.loads(req.text)
86 | #pprint(data)
87 | if "error" in data and "Request had invalid authentication credentials" in data["error"]["message"]:
88 | exit("[-] Cookies/Tokens seems expired, please verify them.")
89 | elif "error" in data:
90 | print("[-] Error :")
91 | pprint(data)
92 | exit()
93 | elif not "matches" in data:
94 | exit("[-] This email address does not belong to a Google Account.")
95 |
96 | return data
97 |
98 | def get_account_data(httpx_client, gaiaID, internal_auth, internal_token, config):
99 | # Bypass method
100 | req_headers = {
101 | "Origin": "https://drive.google.com",
102 | "authorization": internal_auth,
103 | "Host": "people-pa.clients6.google.com"
104 | }
105 | headers = {**config.headers, **req_headers}
106 |
107 | url = f"https://people-pa.clients6.google.com/v2/people?person_id={gaiaID}&request_mask.include_container=PROFILE&request_mask.include_container=DOMAIN_PROFILE&request_mask.include_field.paths=person.metadata.best_display_name&request_mask.include_field.paths=person.photo&request_mask.include_field.paths=person.cover_photo&request_mask.include_field.paths=person.email&request_mask.include_field.paths=person.organization&request_mask.include_field.paths=person.location&request_mask.include_field.paths=person.email&requestMask.includeField.paths=person.phone&core_id_params.enable_private_names=true&requestMask.includeField.paths=person.read_only_profile_info&key={internal_token}"
108 | req = httpx_client.get(url, headers=headers)
109 | data = json.loads(req.text)
110 | # pprint(data)
111 | if "error" in data and "Request had invalid authentication credentials" in data["error"]["message"]:
112 | exit("[-] Cookies/Tokens seems expired, please verify them.")
113 | elif "error" in data:
114 | print("[-] Error :")
115 | pprint(data)
116 | exit()
117 | if data["personResponse"][0]["status"].lower() == "not_found":
118 | return False
119 |
120 | name = get_account_name(httpx_client, gaiaID, data, internal_auth, internal_token, config)
121 |
122 | profile_data = data["personResponse"][0]["person"]
123 |
124 | profile_pics = []
125 | for p in profile_data["photo"]:
126 | profile_pics.append(Picture(p["url"], p.get("isDefault", False)))
127 |
128 | # mostly is default
129 | cover_pics = []
130 | for p in profile_data["coverPhoto"]:
131 | cover_pics.append(Picture(p["imageUrl"], p["isDefault"]))
132 |
133 | emails = update_emails({}, profile_data)
134 |
135 | # absent if user didn't enter or hide them
136 | phones = []
137 | if "phone" in profile_data:
138 | for p in profile_data["phone"]:
139 | phones.append(f'{p["value"]} ({p["type"]})')
140 |
141 | # absent if user didn't enter or hide them
142 | locations = []
143 | if "location" in profile_data:
144 | for l in profile_data["location"]:
145 | locations.append(l["value"] if not l.get("current") else f'{l["value"]} (current)')
146 |
147 | # absent if user didn't enter or hide them
148 | organizations = []
149 | if "organization" in profile_data:
150 | organizations = (f'{o["name"]} ({o["type"]})' for o in profile_data["organization"])
151 |
152 | return {"name": name, "profile_pics": profile_pics, "cover_pics": cover_pics,
153 | "organizations": ', '.join(organizations), "locations": ', '.join(locations),
154 | "emails_set": emails, "phones": ', '.join(phones)}
155 |
156 | def get_account_name(httpx_client, gaiaID, data, internal_auth, internal_token, config):
157 | try:
158 | name = data["personResponse"][0]["person"]["metadata"]["bestDisplayName"]["displayName"]
159 | except KeyError:
160 | pass # We fallback on the classic method
161 | else:
162 | return name
163 |
164 | # Classic method, but requires the target to have at least 1 GMaps contribution
165 | req = httpx_client.get(f"https://www.google.com/maps/contrib/{gaiaID}")
166 | gmaps_source = req.text
167 | match = re.search(r'', gmaps_source)
168 | if not match:
169 | return None
170 | return match[1]
171 |
172 | def image_hash(img):
173 | flathash = imagehash.average_hash(img)
174 | return flathash
175 |
176 | def detect_default_profile_pic(flathash):
177 | if flathash - imagehash.hex_to_flathash("000018183c3c0000", 8) < 10 :
178 | return True
179 | return False
180 |
181 | def sanitize_location(location):
182 | not_country = False
183 | not_town = False
184 | town = "?"
185 | country = "?"
186 | if "city" in location:
187 | town = location["city"]
188 | elif "village" in location:
189 | town = location["village"]
190 | elif "town" in location:
191 | town = location["town"]
192 | elif "municipality" in location:
193 | town = location["municipality"]
194 | else:
195 | not_town = True
196 | if not "country" in location:
197 | not_country = True
198 | location["country"] = country
199 | if not_country and not_town:
200 | return False
201 | location["town"] = town
202 | return location
203 |
204 |
205 | def get_driverpath():
206 | driver_path = shutil.which("chromedriver")
207 | if driver_path:
208 | return driver_path
209 | if within_docker():
210 | chromedrivermanager_silent = ChromeDriverManager(print_first_line=False, log_level=0, path="/usr/src/app")
211 | else:
212 | chromedrivermanager_silent = ChromeDriverManager(print_first_line=False, log_level=0)
213 | driver = chromedrivermanager_silent.driver
214 | driverpath_with_version = chromedrivermanager_silent.driver_cache.find_driver(driver.browser_version, driver.get_name(), driver.get_os_type(), driver.get_version())
215 | driverpath_without_version = chromedrivermanager_silent.driver_cache.find_driver("", driver.get_name(), driver.get_os_type(), "")
216 | if driverpath_with_version:
217 | return driverpath_with_version
218 | elif not driverpath_with_version and driverpath_without_version:
219 | print("[Webdrivers Manager] I'm updating the chromedriver...")
220 | if within_docker():
221 | driver_path = ChromeDriverManager(path="/usr/src/app").install()
222 | else:
223 | driver_path = ChromeDriverManager().install()
224 | print("[Webdrivers Manager] The chromedriver has been updated !\n")
225 | else:
226 | print("[Webdrivers Manager] I can't find the chromedriver, so I'm downloading and installing it for you...")
227 | if within_docker():
228 | driver_path = ChromeDriverManager(path="/usr/src/app").install()
229 | else:
230 | driver_path = ChromeDriverManager().install()
231 | print("[Webdrivers Manager] The chromedriver has been installed !\n")
232 | return driver_path
233 |
234 |
235 | def get_chrome_options_args(is_headless):
236 | chrome_options = Options()
237 | chrome_options.add_argument('--log-level=3')
238 | chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
239 | chrome_options.add_argument("--no-sandbox")
240 | if is_headless:
241 | chrome_options.add_argument("--headless")
242 | if (Os().wsl or Os().windows) and is_headless:
243 | chrome_options.add_argument("--disable-gpu")
244 | chrome_options.add_argument("--disable-dev-shm-usage")
245 | chrome_options.add_argument("--disable-setuid-sandbox")
246 | chrome_options.add_argument("--no-first-run")
247 | chrome_options.add_argument("--no-zygote")
248 | chrome_options.add_argument("--single-process")
249 | chrome_options.add_argument("--disable-features=VizDisplayCompositor")
250 | return chrome_options
251 |
252 | def inject_osid(cookies, service, config):
253 | with open(config.data_path, 'r') as f:
254 | out = json.loads(f.read())
255 |
256 | cookies["OSID"] = out["osids"][service]
257 | return cookies
--------------------------------------------------------------------------------
/lib/youtube.py:
--------------------------------------------------------------------------------
1 | import json
2 | import urllib.parse
3 | from io import BytesIO
4 | from urllib.parse import unquote as parse_url
5 |
6 | from PIL import Image
7 |
8 | from lib.search import search as gdoc_search
9 | from lib.utils import *
10 |
11 |
12 | def get_channel_data(client, channel_url):
13 | data = None
14 |
15 | retries = 2
16 | for retry in list(range(retries))[::-1]:
17 | req = client.get(f"{channel_url}/about")
18 | source = req.text
19 | try:
20 | data = json.loads(source.split('var ytInitialData = ')[1].split(';')[0])
21 | except (KeyError, IndexError):
22 | if retry == 0:
23 | return False
24 | continue
25 | else:
26 | break
27 |
28 | handle = data["metadata"]["channelMetadataRenderer"]["vanityChannelUrl"].split("/")[-1]
29 | tabs = [x[list(x.keys())[0]] for x in data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]]
30 | about_tab = [x for x in tabs if x["title"].lower() == "about"][0]
31 | channel_details = about_tab["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"][0]["channelAboutFullMetadataRenderer"]
32 |
33 | out = {
34 | "name": None,
35 | "description": None,
36 | "channel_urls": [],
37 | "email_contact": False,
38 | "views": None,
39 | "joined_date": None,
40 | "primary_links": [],
41 | "country": None
42 | }
43 |
44 | out["name"] = data["metadata"]["channelMetadataRenderer"]["title"]
45 |
46 | out["channel_urls"].append(data["metadata"]["channelMetadataRenderer"]["channelUrl"])
47 | out["channel_urls"].append(f"https://www.youtube.com/c/{handle}")
48 | out["channel_urls"].append(f"https://www.youtube.com/user/{handle}")
49 |
50 | out["email_contact"] = "businessEmailLabel" in channel_details
51 |
52 | out["description"] = channel_details["description"]["simpleText"] if "description" in channel_details else None
53 | out["views"] = channel_details["viewCountText"]["simpleText"].split(" ")[0] if "viewCountText" in channel_details else None
54 | out["joined_date"] = channel_details["joinedDateText"]["runs"][1]["text"] if "joinedDateText" in channel_details else None
55 | out["country"] = channel_details["country"]["simpleText"] if "country" in channel_details else None
56 |
57 | if "primaryLinks" in channel_details:
58 | for primary_link in channel_details["primaryLinks"]:
59 | title = primary_link["title"]["simpleText"]
60 | url = parse_url(primary_link["navigationEndpoint"]["urlEndpoint"]["url"].split("&q=")[-1])
61 | out["primary_links"].append({"title": title, "url": url})
62 |
63 | return out
64 |
65 | def youtube_channel_search(client, query):
66 | try:
67 | link = "https://www.youtube.com/results?search_query={}&sp=EgIQAg%253D%253D"
68 | req = client.get(link.format(urllib.parse.quote(query)))
69 | source = req.text
70 | data = json.loads(
71 | source.split('window["ytInitialData"] = ')[1].split('window["ytInitialPlayerResponse"]')[0].split(';\n')[0])
72 | channels = \
73 | data["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"]["sectionListRenderer"]["contents"][0][
74 | "itemSectionRenderer"]["contents"]
75 | results = {"channels": [], "length": len(channels)}
76 | for channel in channels:
77 | if len(results["channels"]) >= 10:
78 | break
79 | title = channel["channelRenderer"]["title"]["simpleText"]
80 | if not query.lower() in title.lower():
81 | continue
82 | avatar_link = channel["channelRenderer"]["thumbnail"]["thumbnails"][0]["url"].split('=')[0]
83 | if avatar_link[:2] == "//":
84 | avatar_link = "https:" + avatar_link
85 | profile_url = "https://youtube.com" + channel["channelRenderer"]["navigationEndpoint"]["browseEndpoint"][
86 | "canonicalBaseUrl"]
87 | req = client.get(avatar_link)
88 | img = Image.open(BytesIO(req.content))
89 | hash = str(image_hash(img))
90 | results["channels"].append({"profile_url": profile_url, "name": title, "hash": hash})
91 | return results
92 | except (KeyError, IndexError):
93 | return False
94 |
95 |
96 | def youtube_channel_search_gdocs(client, query, data_path, gdocs_public_doc):
97 | search_query = f"site:youtube.com/channel \\\"{query}\\\""
98 | search_results = gdoc_search(search_query, data_path, gdocs_public_doc)
99 | channels = []
100 |
101 | for result in search_results:
102 | sanitized = "https://youtube.com/" + ('/'.join(result["link"].split('/')[3:5]).split("?")[0])
103 | if sanitized not in channels:
104 | channels.append(sanitized)
105 |
106 | if not channels:
107 | return False
108 |
109 | results = {"channels": [], "length": len(channels)}
110 | channels = channels[:5]
111 |
112 | for profile_url in channels:
113 | data = None
114 | avatar_link = None
115 |
116 | retries = 2
117 | for retry in list(range(retries))[::-1]:
118 | req = client.get(profile_url, follow_redirects=True)
119 | source = req.text
120 | try:
121 | data = json.loads(source.split('var ytInitialData = ')[1].split(';')[0])
122 | avatar_link = data["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].split('=')[0]
123 | except (KeyError, IndexError) as e:
124 | #import pdb; pdb.set_trace()
125 | if retry == 0:
126 | return False
127 | continue
128 | else:
129 | break
130 | req = client.get(avatar_link)
131 | img = Image.open(BytesIO(req.content))
132 | hash = str(image_hash(img))
133 | title = data["metadata"]["channelMetadataRenderer"]["title"]
134 | results["channels"].append({"profile_url": profile_url, "name": title, "hash": hash})
135 | return results
136 |
137 |
138 | def get_channels(client, query, data_path, gdocs_public_doc):
139 | from_youtube = youtube_channel_search(client, query)
140 | from_gdocs = youtube_channel_search_gdocs(client, query, data_path, gdocs_public_doc)
141 | to_process = []
142 | if from_youtube:
143 | from_youtube["origin"] = "youtube"
144 | to_process.append(from_youtube)
145 | if from_gdocs:
146 | from_gdocs["origin"] = "gdocs"
147 | to_process.append(from_gdocs)
148 | if not to_process:
149 | return False
150 | return to_process
151 |
152 |
153 | def get_confidence(data, query, hash):
154 | score_steps = 4
155 |
156 | for source_nb, source in enumerate(data):
157 | for channel_nb, channel in enumerate(source["channels"]):
158 | score = 0
159 |
160 | if hash == imagehash.hex_to_flathash(channel["hash"], 8):
161 | score += score_steps * 4
162 | if query == channel["name"]:
163 | score += score_steps * 3
164 | if query in channel["name"]:
165 | score += score_steps * 2
166 | if ((source["origin"] == "youtube" and source["length"] <= 5) or
167 | (source["origin"] == "google" and source["length"] <= 4)):
168 | score += score_steps
169 | data[source_nb]["channels"][channel_nb]["score"] = score
170 |
171 | channels = []
172 | for source in data:
173 | for channel in source["channels"]:
174 | found_better = False
175 | for source2 in data:
176 | for channel2 in source["channels"]:
177 | if channel["profile_url"] == channel2["profile_url"]:
178 | if channel2["score"] > channel["score"]:
179 | found_better = True
180 | break
181 | if found_better:
182 | break
183 | if found_better:
184 | continue
185 | else:
186 | channels.append(channel)
187 | channels = sorted([json.loads(chan) for chan in set([json.dumps(channel) for channel in channels])],
188 | key=lambda k: k['score'], reverse=True)
189 | panels = sorted(set([c["score"] for c in channels]), reverse=True)
190 | if not channels or (panels and panels[0] <= 0):
191 | return 0, []
192 |
193 | maxscore = sum([p * score_steps for p in range(1, score_steps + 1)])
194 | for panel in panels:
195 | chans = [c for c in channels if c["score"] == panel]
196 | if len(chans) > 1:
197 | panel -= 5
198 | return (panel / maxscore * 100), chans
199 |
200 |
201 | def extract_usernames(channels):
202 | return [chan['profile_url'].split("/user/")[1] for chan in channels if "/user/" in chan['profile_url']]
203 |
--------------------------------------------------------------------------------
/modules/doc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import json
4 | import sys
5 | import os
6 | from datetime import datetime
7 | from io import BytesIO
8 | from os.path import isfile
9 | from pathlib import Path
10 | from pprint import pprint
11 |
12 | import httpx
13 | from PIL import Image
14 |
15 | import config
16 | from lib.utils import *
17 | from lib.banner import banner
18 |
19 |
20 | def doc_hunt(doc_link):
21 | banner()
22 |
23 | tmprinter = TMPrinter()
24 |
25 | if not doc_link:
26 | exit("Please give the link to a Google resource.\nExample : https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms")
27 |
28 | is_within_docker = within_docker()
29 | if is_within_docker:
30 | print("[+] Docker detected, profile pictures will not be saved.")
31 |
32 | doc_id = ''.join([x for x in doc_link.split("?")[0].split("/") if len(x) in (33, 44)])
33 | if doc_id:
34 | print(f"\nDocument ID : {doc_id}\n")
35 | else:
36 | exit("\nDocument ID not found.\nPlease make sure you have something that looks like this in your link :\1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms")
37 |
38 | if not isfile(config.data_path):
39 | exit("Please generate cookies and tokens first, with the check_and_gen.py script.")
40 |
41 | internal_token = ""
42 | cookies = {}
43 |
44 | with open(config.data_path, 'r') as f:
45 | out = json.loads(f.read())
46 | internal_token = out["keys"]["internal"]
47 | cookies = out["cookies"]
48 |
49 | headers = {**config.headers, **{"X-Origin": "https://drive.google.com"}}
50 | client = httpx.Client(cookies=cookies, headers=headers)
51 |
52 | url = f"https://clients6.google.com/drive/v2beta/files/{doc_id}?fields=alternateLink%2CcopyRequiresWriterPermission%2CcreatedDate%2Cdescription%2CdriveId%2CfileSize%2CiconLink%2Cid%2Clabels(starred%2C%20trashed)%2ClastViewedByMeDate%2CmodifiedDate%2Cshared%2CteamDriveId%2CuserPermission(id%2Cname%2CemailAddress%2Cdomain%2Crole%2CadditionalRoles%2CphotoLink%2Ctype%2CwithLink)%2Cpermissions(id%2Cname%2CemailAddress%2Cdomain%2Crole%2CadditionalRoles%2CphotoLink%2Ctype%2CwithLink)%2Cparents(id)%2Ccapabilities(canMoveItemWithinDrive%2CcanMoveItemOutOfDrive%2CcanMoveItemOutOfTeamDrive%2CcanAddChildren%2CcanEdit%2CcanDownload%2CcanComment%2CcanMoveChildrenWithinDrive%2CcanRename%2CcanRemoveChildren%2CcanMoveItemIntoTeamDrive)%2Ckind&supportsTeamDrives=true&enforceSingleParent=true&key={internal_token}"
53 |
54 | retries = 100
55 | for retry in range(retries):
56 | req = client.get(url)
57 | if "File not found" in req.text:
58 | exit("[-] This file does not exist or is not public")
59 | elif "rateLimitExceeded" in req.text:
60 | tmprinter.out(f"[-] Rate-limit detected, retrying... {retry+1}/{retries}")
61 | continue
62 | else:
63 | break
64 | else:
65 | tmprinter.clear()
66 | exit("[-] Rate-limit exceeded. Try again later.")
67 |
68 | if '"reason": "keyInvalid"' in req.text:
69 | exit("[-] Your key is invalid, try regenerating your cookies & keys.")
70 |
71 | tmprinter.clear()
72 | data = json.loads(req.text)
73 |
74 | # Extracting informations
75 |
76 | # Dates
77 |
78 | created_date = datetime.strptime(data["createdDate"], '%Y-%m-%dT%H:%M:%S.%fz')
79 | modified_date = datetime.strptime(data["modifiedDate"], '%Y-%m-%dT%H:%M:%S.%fz')
80 |
81 | print(f"[+] Creation date : {created_date.strftime('%Y/%m/%d %H:%M:%S')} (UTC)")
82 | print(f"[+] Last edit date : {modified_date.strftime('%Y/%m/%d %H:%M:%S')} (UTC)")
83 |
84 | # Permissions
85 |
86 | user_permissions = []
87 | if data["userPermission"]:
88 | if data["userPermission"]["id"] == "me":
89 | user_permissions.append(data["userPermission"]["role"])
90 | if "additionalRoles" in data["userPermission"]:
91 | user_permissions += data["userPermission"]["additionalRoles"]
92 |
93 | public_permissions = []
94 | owner = None
95 | for permission in data["permissions"]:
96 | if permission["id"] in ["anyoneWithLink", "anyone"]:
97 | public_permissions.append(permission["role"])
98 | if "additionalRoles" in data["permissions"]:
99 | public_permissions += permission["additionalRoles"]
100 | elif permission["role"] == "owner":
101 | owner = permission
102 |
103 | print("\nPublic permissions :")
104 | for permission in public_permissions:
105 | print(f"- {permission}")
106 |
107 | if public_permissions != user_permissions:
108 | print("[+] You have special permissions :")
109 | for permission in user_permissions:
110 | print(f"- {permission}")
111 |
112 | if owner:
113 | print("\n[+] Owner found !\n")
114 | print(f"Name : {owner['name']}")
115 | print(f"Email : {owner['emailAddress']}")
116 | print(f"Google ID : {owner['id']}")
117 |
118 | # profile picture
119 | profile_pic_link = owner['photoLink']
120 | req = client.get(profile_pic_link)
121 |
122 | profile_pic_img = Image.open(BytesIO(req.content))
123 | profile_pic_flathash = image_hash(profile_pic_img)
124 | is_default_profile_pic = detect_default_profile_pic(profile_pic_flathash)
125 |
126 | if not is_default_profile_pic and not is_within_docker:
127 | print("\n[+] Custom profile picture !")
128 | print(f"=> {profile_pic_link}")
129 | if config.write_profile_pic and not is_within_docker:
130 | open(Path(config.profile_pics_dir) / f'{owner["emailAddress"]}.jpg', 'wb').write(req.content)
131 | print("Profile picture saved !\n")
132 | else:
133 | print("\n[-] Default profile picture\n")
--------------------------------------------------------------------------------
/modules/email.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import json
4 | import sys
5 | import os
6 | from datetime import datetime
7 | from io import BytesIO
8 | from os.path import isfile
9 | from pathlib import Path
10 | from pprint import pprint
11 |
12 | import httpx
13 | from PIL import Image
14 | from geopy.geocoders import Nominatim
15 |
16 | import config
17 | from lib.banner import banner
18 | import lib.gmaps as gmaps
19 | import lib.youtube as ytb
20 | from lib.photos import gpics
21 | from lib.utils import *
22 | import lib.calendar as gcalendar
23 |
24 |
25 | def email_hunt(email):
26 | banner()
27 |
28 | if not email:
29 | exit("Please give a valid email.\nExample : larry@google.com")
30 |
31 | if not isfile(config.data_path):
32 | exit("Please generate cookies and tokens first, with the check_and_gen.py script.")
33 |
34 | hangouts_auth = ""
35 | hangouts_token = ""
36 | internal_auth = ""
37 | internal_token = ""
38 |
39 | cookies = {}
40 |
41 | with open(config.data_path, 'r') as f:
42 | out = json.loads(f.read())
43 | hangouts_auth = out["hangouts_auth"]
44 | hangouts_token = out["keys"]["hangouts"]
45 | internal_auth = out["internal_auth"]
46 | internal_token = out["keys"]["internal"]
47 | cookies = out["cookies"]
48 |
49 | client = httpx.Client(cookies=cookies, headers=config.headers)
50 |
51 | data = is_email_google_account(client, hangouts_auth, cookies, email,
52 | hangouts_token)
53 |
54 | is_within_docker = within_docker()
55 | if is_within_docker:
56 | print("[+] Docker detected, profile pictures will not be saved.")
57 |
58 | geolocator = Nominatim(user_agent="nominatim")
59 | print(f"[+] {len(data['matches'])} account found !")
60 |
61 | for user in data["matches"]:
62 | print("\n------------------------------\n")
63 |
64 | gaiaID = user["personId"][0]
65 | email = user["lookupId"]
66 | infos = data["people"][gaiaID]
67 |
68 | # get name & profile picture
69 | account = get_account_data(client, gaiaID, internal_auth, internal_token, config)
70 | name = account["name"]
71 |
72 | if name:
73 | print(f"Name : {name}")
74 | else:
75 | if "name" not in infos:
76 | print("[-] Couldn't find name")
77 | else:
78 | for i in range(len(infos["name"])):
79 | if 'displayName' in infos['name'][i].keys():
80 | name = infos["name"][i]["displayName"]
81 | print(f"Name : {name}")
82 |
83 | organizations = account["organizations"]
84 | if organizations:
85 | print(f"Organizations : {organizations}")
86 |
87 | locations = account["locations"]
88 | if locations:
89 | print(f"Locations : {locations}")
90 |
91 | # profile picture
92 | profile_pic_url = account.get("profile_pics") and account["profile_pics"][0].url
93 | if profile_pic_url:
94 | req = client.get(profile_pic_url)
95 |
96 | # TODO: make sure it's necessary now
97 | profile_pic_img = Image.open(BytesIO(req.content))
98 | profile_pic_flathash = image_hash(profile_pic_img)
99 | is_default_profile_pic = detect_default_profile_pic(profile_pic_flathash)
100 |
101 | if not is_default_profile_pic:
102 | print("\n[+] Custom profile picture !")
103 | print(f"=> {profile_pic_url}")
104 | if config.write_profile_pic and not is_within_docker:
105 | open(Path(config.profile_pics_dir) / f'{email}.jpg', 'wb').write(req.content)
106 | print("Profile picture saved !")
107 | else:
108 | print("\n[-] Default profile picture")
109 |
110 | # cover profile picture
111 | cover_pic = account.get("cover_pics") and account["cover_pics"][0]
112 | if cover_pic and not cover_pic.is_default:
113 | cover_pic_url = cover_pic.url
114 | req = client.get(cover_pic_url)
115 |
116 | print("\n[+] Custom profile cover picture !")
117 | print(f"=> {cover_pic_url}")
118 | if config.write_profile_pic and not is_within_docker:
119 | open(Path(config.profile_pics_dir) / f'cover_{email}.jpg', 'wb').write(req.content)
120 | print("Cover profile picture saved !")
121 |
122 | # last edit
123 | try:
124 | timestamp = int(infos["metadata"]["lastUpdateTimeMicros"][:-3])
125 | last_edit = datetime.utcfromtimestamp(timestamp).strftime("%Y/%m/%d %H:%M:%S (UTC)")
126 | print(f"\nLast profile edit : {last_edit}")
127 | except KeyError:
128 | last_edit = None
129 | print(f"\nLast profile edit : Not found")
130 |
131 | canonical_email = ""
132 | emails = update_emails(account["emails_set"], infos)
133 | if emails and len(list(emails)) == 1:
134 | if list(emails.values())[0].is_normalized(email):
135 | new_email = list(emails.keys())[0]
136 | if email != new_email:
137 | canonical_email = f' (canonical email is {new_email})'
138 | emails = []
139 |
140 | print(f"\nEmail : {email}{canonical_email}\nGaia ID : {gaiaID}\n")
141 |
142 | if emails:
143 | print(f"Contact emails : {', '.join(map(str, emails.values()))}")
144 |
145 | phones = account["phones"]
146 | if phones:
147 | print(f"Contact phones : {phones}")
148 |
149 | # is bot?
150 | if "extendedData" in infos:
151 | isBot = infos["extendedData"]["hangoutsExtendedData"]["isBot"]
152 | if isBot:
153 | print("Hangouts Bot : Yes !")
154 | else:
155 | print("Hangouts Bot : No")
156 | else:
157 | print("Hangouts Bot : Unknown")
158 |
159 | # decide to check YouTube
160 | ytb_hunt = False
161 | try:
162 | services = [x["appType"].lower() if x["appType"].lower() != "babel" else "hangouts" for x in
163 | infos["inAppReachability"]]
164 | if name and (config.ytb_hunt_always or "youtube" in services):
165 | ytb_hunt = True
166 | print("\n[+] Activated Google services :")
167 | print('\n'.join(["- " + x.capitalize() for x in services]))
168 |
169 | except KeyError:
170 | ytb_hunt = True
171 | print("\n[-] Unable to fetch connected Google services.")
172 |
173 | # check YouTube
174 | if name and ytb_hunt:
175 | confidence = None
176 | data = ytb.get_channels(client, name, config.data_path,
177 | config.gdocs_public_doc)
178 | if not data:
179 | print("\n[-] YouTube channel not found.")
180 | else:
181 | confidence, channels = ytb.get_confidence(data, name, profile_pic_flathash)
182 |
183 | if confidence:
184 | print(f"\n[+] YouTube channel (confidence => {confidence}%) :")
185 | for channel in channels:
186 | print(f"- [{channel['name']}] {channel['profile_url']}")
187 | possible_usernames = ytb.extract_usernames(channels)
188 | if possible_usernames:
189 | print("\n[+] Possible usernames found :")
190 | for username in possible_usernames:
191 | print(f"- {username}")
192 | else:
193 | print("\n[-] YouTube channel not found.")
194 |
195 | # TODO: return gpics function output here
196 | #gpics(gaiaID, client, cookies, config.headers, config.regexs["albums"], config.regexs["photos"],
197 | # config.headless)
198 |
199 | # reviews
200 | reviews = gmaps.scrape(gaiaID, client, cookies, config, config.headers, config.regexs["review_loc_by_id"], config.headless)
201 |
202 | if reviews:
203 | confidence, locations = gmaps.get_confidence(geolocator, reviews, config.gmaps_radius)
204 | print(f"\n[+] Probable location (confidence => {confidence}) :")
205 |
206 | loc_names = []
207 | for loc in locations:
208 | loc_names.append(
209 | f"- {loc['avg']['town']}, {loc['avg']['country']}"
210 | )
211 |
212 | loc_names = set(loc_names) # delete duplicates
213 | for loc in loc_names:
214 | print(loc)
215 |
216 |
217 | # Google Calendar
218 | calendar_response = gcalendar.fetch(email, client, config)
219 | if calendar_response:
220 | print("[+] Public Google Calendar found !")
221 | events = calendar_response["events"]
222 | if events:
223 | gcalendar.out(events)
224 | else:
225 | print("=> No recent events found.")
226 | else:
227 | print("[-] No public Google Calendar.")
228 |
--------------------------------------------------------------------------------
/modules/gaia.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import json
4 | import sys
5 | import os
6 | from datetime import datetime
7 | from io import BytesIO
8 | from os.path import isfile
9 | from pathlib import Path
10 | from pprint import pprint
11 |
12 | import httpx
13 | from PIL import Image
14 | from geopy.geocoders import Nominatim
15 |
16 | import config
17 | from lib.banner import banner
18 | import lib.gmaps as gmaps
19 | import lib.youtube as ytb
20 | from lib.utils import *
21 |
22 |
23 | def gaia_hunt(gaiaID):
24 | banner()
25 |
26 | if not gaiaID:
27 | exit("Please give a valid GaiaID.\nExample : 113127526941309521065")
28 |
29 | if not isfile(config.data_path):
30 | exit("Please generate cookies and tokens first, with the check_and_gen.py script.")
31 |
32 | internal_auth = ""
33 | internal_token = ""
34 |
35 | cookies = {}
36 |
37 | with open(config.data_path, 'r') as f:
38 | out = json.loads(f.read())
39 | internal_auth = out["internal_auth"]
40 | internal_token = out["keys"]["internal"]
41 | cookies = out["cookies"]
42 |
43 | client = httpx.Client(cookies=cookies, headers=config.headers)
44 |
45 | account = get_account_data(client, gaiaID, internal_auth, internal_token, config)
46 | if not account:
47 | exit("[-] No account linked to this Gaia ID.")
48 |
49 | is_within_docker = within_docker()
50 | if is_within_docker:
51 | print("[+] Docker detected, profile pictures will not be saved.")
52 |
53 | geolocator = Nominatim(user_agent="nominatim")
54 |
55 | # get name & other info
56 | name = account["name"]
57 | if name:
58 | print(f"Name : {name}")
59 |
60 | organizations = account["organizations"]
61 | if organizations:
62 | print(f"Organizations : {organizations}")
63 |
64 | locations = account["locations"]
65 | if locations:
66 | print(f"Locations : {locations}")
67 |
68 | # get profile picture
69 | profile_pic_url = account.get("profile_pics") and account["profile_pics"][0].url
70 | if profile_pic_url:
71 | req = client.get(profile_pic_url)
72 |
73 | # TODO: make sure it's necessary now
74 | profile_pic_img = Image.open(BytesIO(req.content))
75 | profile_pic_flathash = image_hash(profile_pic_img)
76 | is_default_profile_pic = detect_default_profile_pic(profile_pic_flathash)
77 |
78 | if not is_default_profile_pic:
79 | print("\n[+] Custom profile picture !")
80 | print(f"=> {profile_pic_url}")
81 | if config.write_profile_pic and not is_within_docker:
82 | open(Path(config.profile_pics_dir) / f'{gaiaID}.jpg', 'wb').write(req.content)
83 | print("Profile picture saved !")
84 | else:
85 | print("\n[-] Default profile picture")
86 |
87 | # cover profile picture
88 | cover_pic = account.get("cover_pics") and account["cover_pics"][0]
89 | if cover_pic and not cover_pic.is_default:
90 | req = client.get(cover_pic_url)
91 |
92 | print("\n[+] Custom profile cover picture !")
93 | print(f"=> {cover_pic_url}")
94 | if config.write_profile_pic and not is_within_docker:
95 | open(Path(config.profile_pics_dir) / f'cover_{email}.jpg', 'wb').write(req.content)
96 | print("Cover profile picture saved !")
97 |
98 |
99 | print(f"\nGaia ID : {gaiaID}")
100 |
101 | emails = account["emails_set"]
102 | if emails:
103 | print(f"Contact emails : {', '.join(map(str, emails.values()))}")
104 |
105 | phones = account["phones"]
106 | if phones:
107 | print(f"Contact phones : {phones}")
108 |
109 | # check YouTube
110 | if name:
111 | confidence = None
112 | data = ytb.get_channels(client, name, config.data_path,
113 | config.gdocs_public_doc)
114 | if not data:
115 | print("\n[-] YouTube channel not found.")
116 | else:
117 | confidence, channels = ytb.get_confidence(data, name, profile_pic_flathash)
118 |
119 | if confidence:
120 | print(f"\n[+] YouTube channel (confidence => {confidence}%) :")
121 | for channel in channels:
122 | print(f"- [{channel['name']}] {channel['profile_url']}")
123 | possible_usernames = ytb.extract_usernames(channels)
124 | if possible_usernames:
125 | print("\n[+] Possible usernames found :")
126 | for username in possible_usernames:
127 | print(f"- {username}")
128 | else:
129 | print("\n[-] YouTube channel not found.")
130 |
131 | # reviews
132 | reviews = gmaps.scrape(gaiaID, client, cookies, config, config.headers, config.regexs["review_loc_by_id"], config.headless)
133 |
134 | if reviews:
135 | confidence, locations = gmaps.get_confidence(geolocator, reviews, config.gmaps_radius)
136 | print(f"\n[+] Probable location (confidence => {confidence}) :")
137 |
138 | loc_names = []
139 | for loc in locations:
140 | loc_names.append(
141 | f"- {loc['avg']['town']}, {loc['avg']['country']}"
142 | )
143 |
144 | loc_names = set(loc_names) # delete duplicates
145 | for loc in loc_names:
146 | print(loc)
147 |
--------------------------------------------------------------------------------
/modules/youtube.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import json
4 | import sys
5 | from datetime import datetime
6 | from datetime import date
7 | from io import BytesIO
8 | from os.path import isfile
9 | from pathlib import Path
10 | from pprint import pprint
11 |
12 | import httpx
13 | import wayback
14 | from PIL import Image
15 | from bs4 import BeautifulSoup as bs
16 | from geopy.geocoders import Nominatim
17 |
18 | import config
19 | from lib.banner import banner
20 | import lib.gmaps as gmaps
21 | import lib.youtube as ytb
22 | from lib.utils import *
23 |
24 |
25 | def find_gaiaID(body):
26 | """
27 | We don't use a regex to avoid extracting an other gaiaID
28 | for example if the target had put a secondary Google Plus blog in his channel social links.
29 | """
30 |
31 | # 1st method ~ 2014
32 | try:
33 | publisher = body.find("link", {"rel": "publisher"})
34 | gaiaID = publisher.attrs["href"].split("/")[-1]
35 | except:
36 | pass
37 | else:
38 | if gaiaID:
39 | return gaiaID
40 |
41 | # 2nd method ~ 2015
42 | try:
43 | author_links = [x.find_next("link") for x in body.find_all("span", {"itemprop": "author"})]
44 | valid_author_link = [x for x in author_links if "plus.google.com/" in x.attrs["href"]][0]
45 | gaiaID = valid_author_link.attrs["href"].split("/")[-1]
46 | except:
47 | pass
48 | else:
49 | if gaiaID:
50 | return gaiaID
51 |
52 | # 3rd method ~ 2019
53 | try:
54 | data = json.loads(str(body).split('window["ytInitialData"] = ')[1].split('window["ytInitialPlayerResponse"]')[0].strip().strip(";"))
55 | gaiaID = data["metadata"]["channelMetadataRenderer"]["plusPageLink"].split("/")[-1]
56 | except:
57 | pass
58 | else:
59 | if gaiaID:
60 | return gaiaID
61 |
62 | def analyze_snapshots(client, wb_client, channel_url, dates):
63 | body = None
64 | record = None
65 | for record in wb_client.search(channel_url, to_date=dates["to"], from_date=dates["from"]):
66 | try:
67 | req = client.get(record.raw_url)
68 | if req.status_code == 429:
69 | continue # Rate-limit is fucked up and is snapshot-based, we can just take the next snapshot
70 | except Exception as err:
71 | pass
72 | else:
73 | if re.compile(config.regexs["gplus"]).findall(req.text):
74 | body = bs(req.text, 'html.parser')
75 | #print(record)
76 | print(f'[+] Snapshot : {record.timestamp.strftime("%d/%m/%Y")}')
77 | break
78 | else:
79 | return None
80 |
81 | gaiaID = find_gaiaID(body)
82 | return gaiaID
83 |
84 | def check_channel(client, wb_client, channel_url):
85 | # Fast check (no doubt that GaiaID is present in this period)
86 |
87 | dates = {"to": date(2019, 12, 31), "from": date(2014, 1, 1)}
88 | gaiaID = analyze_snapshots(client, wb_client, channel_url, dates)
89 |
90 | # Complete check
91 |
92 | if not gaiaID:
93 | dates = {"to": date(2020, 7, 31), "from": date(2013, 6, 3)}
94 | gaiaID = analyze_snapshots(client, wb_client, channel_url, dates)
95 |
96 | return gaiaID
97 |
98 | def launch_checks(client, wb_client, channel_data):
99 | for channel_url in channel_data["channel_urls"]:
100 | gaiaID = check_channel(client, wb_client, channel_url)
101 | if gaiaID:
102 | return gaiaID
103 |
104 | return False
105 |
106 | def youtube_hunt(channel_url):
107 | banner()
108 |
109 | if not channel_url:
110 | exit("Please give a valid channel URL.\nExample : https://www.youtube.com/user/PewDiePie")
111 |
112 | if not isfile(config.data_path):
113 | exit("Please generate cookies and tokens first, with the check_and_gen.py script.")
114 |
115 | internal_auth = ""
116 | internal_token = ""
117 |
118 | cookies = {}
119 |
120 | with open(config.data_path, 'r') as f:
121 | out = json.loads(f.read())
122 | internal_auth = out["internal_auth"]
123 | internal_token = out["keys"]["internal"]
124 | cookies = out["cookies"]
125 |
126 | if not "PREF" in cookies:
127 | pref_cookies = {"PREF": "tz=Europe.Paris&f6=40000000&hl=en"} # To set the lang in english
128 | cookies = {**cookies, **pref_cookies}
129 |
130 | client = httpx.Client(cookies=cookies, headers=config.headers)
131 |
132 | is_within_docker = within_docker()
133 | if is_within_docker:
134 | print("[+] Docker detected, profile pictures will not be saved.")
135 |
136 | geolocator = Nominatim(user_agent="nominatim")
137 |
138 | print("\n📌 [Youtube channel]")
139 |
140 | channel_data = ytb.get_channel_data(client, channel_url)
141 | if channel_data:
142 | is_channel_existing = True
143 | print(f'[+] Channel name : {channel_data["name"]}\n')
144 | else:
145 | is_channel_existing = False
146 | print("[-] Channel not found.\nSearching for a trace in the archives...\n")
147 |
148 | channel_data = {
149 | "name": None,
150 | "description": None,
151 | "channel_urls": [channel_url],
152 | "email_contact": False,
153 | "views": None,
154 | "joined_date": None,
155 | "primary_links": [],
156 | "country": None
157 | }
158 |
159 | wb_client = wayback.WaybackClient()
160 | gaiaID = launch_checks(client, wb_client, channel_data)
161 | if gaiaID:
162 | print(f"[+] GaiaID => {gaiaID}\n")
163 | else:
164 | print("[-] No interesting snapshot found.\n")
165 |
166 | if is_channel_existing:
167 | if channel_data["email_contact"]:
168 | print(f'[+] Email on profile : available !')
169 | else:
170 | print(f'[-] Email on profile : not available.')
171 | if channel_data["country"]:
172 | print(f'[+] Country : {channel_data["country"]}')
173 | print()
174 | if channel_data["description"]:
175 | print(f'🧬 Description : {channel_data["description"]}')
176 | if channel_data["views"]:
177 | print(f'🧬 Total views : {channel_data["views"]}')
178 | if channel_data["joined_date"]:
179 | print(f'🧬 Joined date : {channel_data["joined_date"]}')
180 |
181 | if channel_data["primary_links"]:
182 | print(f'\n[+] Primary links ({len(channel_data["primary_links"])} found)')
183 | for primary_link in channel_data["primary_links"]:
184 | print(f'- {primary_link["title"]} => {primary_link["url"]}')
185 |
186 |
187 | if not gaiaID:
188 | exit()
189 |
190 | print("\n📌 [Google account]")
191 | # get name & profile picture
192 | account = get_account_data(client, gaiaID, internal_auth, internal_token, config)
193 | name = account["name"]
194 |
195 | if name:
196 | print(f"Name : {name}")
197 |
198 | # profile picture
199 | profile_pic_url = account.get("profile_pics") and account["profile_pics"][0].url
200 | req = client.get(profile_pic_url)
201 |
202 | profile_pic_img = Image.open(BytesIO(req.content))
203 | profile_pic_hash = image_hash(profile_pic_img)
204 | is_default_profile_pic = detect_default_profile_pic(profile_pic_hash)
205 |
206 | if profile_pic_url:
207 | req = client.get(profile_pic_url)
208 |
209 | # TODO: make sure it's necessary now
210 | profile_pic_img = Image.open(BytesIO(req.content))
211 | profile_pic_flathash = image_hash(profile_pic_img)
212 | is_default_profile_pic = detect_default_profile_pic(profile_pic_flathash)
213 |
214 | if not is_default_profile_pic:
215 | print("\n[+] Custom profile picture !")
216 | print(f"=> {profile_pic_url}")
217 | if config.write_profile_pic and not is_within_docker:
218 | open(Path(config.profile_pics_dir) / f'{gaiaID}.jpg', 'wb').write(req.content)
219 | print("Profile picture saved !")
220 | else:
221 | print("\n[-] Default profile picture")
222 |
223 | # cover profile picture
224 | cover_pic = account.get("cover_pics") and account["cover_pics"][0]
225 | if cover_pic and not cover_pic.is_default:
226 | cover_pic_url = cover_pic.url
227 | req = client.get(cover_pic_url)
228 |
229 | print("\n[+] Custom profile cover picture !")
230 | print(f"=> {cover_pic_url}")
231 | if config.write_profile_pic and not is_within_docker:
232 | open(Path(config.profile_pics_dir) / f'cover_{gaiaID}.jpg', 'wb').write(req.content)
233 | print("Cover profile picture saved !")
234 |
235 | # reviews
236 | reviews = gmaps.scrape(gaiaID, client, cookies, config, config.headers, config.regexs["review_loc_by_id"], config.headless)
237 |
238 | if reviews:
239 | confidence, locations = gmaps.get_confidence(geolocator, reviews, config.gmaps_radius)
240 | print(f"\n[+] Probable location (confidence => {confidence}) :")
241 |
242 | loc_names = []
243 | for loc in locations:
244 | loc_names.append(
245 | f"- {loc['avg']['town']}, {loc['avg']['country']}"
246 | )
247 |
248 | loc_names = set(loc_names) # delete duplicates
249 | for loc in loc_names:
250 | print(loc)
251 |
--------------------------------------------------------------------------------
/profile_pics/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soxoj/GHunt/b41322364294850678f4b80f6a5cf0ee36a5dc54/profile_pics/.keep
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | geopy
2 | httpx>=0.20.0
3 | selenium-wire>=4.5.5
4 | selenium>=4.0.0
5 | imagehash
6 | pillow
7 | python-dateutil
8 | colorama
9 | beautifultable
10 | termcolor
11 | webdriver-manager
12 | wayback
13 | bs4
14 | packaging
15 |
--------------------------------------------------------------------------------
/resources/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soxoj/GHunt/b41322364294850678f4b80f6a5cf0ee36a5dc54/resources/.gitkeep
--------------------------------------------------------------------------------