在Python题的getshell途中,我们可以把某个类的成员函数劫持成eval
。看起来一切都没有问题,但仔细想想,调用类的成员函数的时候传参不是会传递self
也就是func(clazz.self, paramter)
吗?那为什么没有self
没有被作为eval
的第一个参数呢,而且还没有报错?
知其然更要知其所以然,我们今天就来以python3.7.8源码为例进行分析。调试python的源码的环境配置步骤和调试php源码的时候差不多,所以可以参考php源码调试环境搭建的相关文章。
小贴士:python源码中Doc文件夹里的存放着官方文档(一些.rst
文件),不知道源码里的函数的作用时,可以在Doc中进行搜索查看,在clion中可以按Ctrl+Shift+F进行全部搜索,搜索范围可达整个项目乃至库函数的源码)。
引例
来看下面的这份代码,我们知道eval()的定义是eval(expression[, globals[, locals]])
。
如果把eval()
放在__eq__
里,在执行a=="bb"
的时候expression
应该传入的是self
,然而globals
应该传的是"bb"
,如果这样的话一定是会报错没法继续执行的。:
class A():
pass
if __name__ == "__main__":
a = A()
a.__class__.__eq__ = eval
print(a)
print(eval)
a == "bb"
那实际中,在__eq__
里用a.__class__.__eq__ = eval
放进eval()
,为什么没有报错,反而正常执行了呢?
分析
0x01 builtin_eval
python语言里的eval()
是属于python
的builtin_function
,它的实现是在builtin_eval
static PyObject *
builtin_eval(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *return_value = NULL;
PyObject *source;
PyObject *globals = Py_None;
PyObject *locals = Py_None;
if (!_PyArg_UnpackStack(args, nargs, "eval",
1, 3,
&source, &globals, &locals)) {
goto exit;
}
return_value = builtin_eval_impl(module, source, globals, locals);
exit:
return return_value;
}
所以对这个函数下断点,看看调用链。
可以看到在解释a == 'bb'
的时候,因为在进行==
比较do_richcompare
发挥了作用,参数中的op=2
意思是正在进行==
在python语言的设计理念里,一个对象拥有诸多个slots
,比如__str__
就是一个槽函数,你可以Override它。包括__eq__
也是
https://docs.python.org/3.8/c-api/typeobj.html?highlight=slots
a.__class__.__eq__=eval
,所以可以理解为将eval
就放在了这个eq对应的slot
里,这样就进入到了slot_tp_richcompare
。
如果没有放eval
,那么python在进行richcompare
的时候就按照正常的流程进行比较。
static PyObject *
slot_tp_richcompare(PyObject *self, PyObject *other, int op)
{
int unbound;
PyObject *func, *res;
func = lookup_maybe_method(self, &name_op[op], &unbound);
if (func == NULL) {
PyErr_Clear();
Py_RETURN_NOTIMPLEMENTED;
}
PyObject *args[1] = {other};
res = call_unbound(unbound, func, self, args, 1);
Py_DECREF(func);
return res;
}
static _Py_Identifier name_op[] = {
{0, "__lt__", 0},
{0, "__le__", 0},
{0, "__eq__", 0},
{0, "__ne__", 0},
{0, "__gt__", 0},
{0, "__ge__", 0}
};
lookup_maybe_method
取出放在__eq__
里的eval,用然后call_unbound
执行eval
但是注意到call_unbound
里还是传入了self
,那self
是在哪被丢掉的?
因为unbound=0
,所以self
在这里被丢掉了
static PyObject*
call_unbound(int unbound, PyObject *func, PyObject *self,
PyObject **args, Py_ssize_t nargs)
{
if (unbound) { //unbound = 0
return _PyObject_FastCall_Prepend(func, self, args, nargs);
}
else {
return _PyObject_FastCall(func, args, nargs);
}
}
现在知道了self
在哪被丢掉的,那么unbound = 0
又是怎么来的,让我们刨根问底,继续接着看:
0x02 unbound
继续跟踪可以发现_PyObject_FastCallDict()
,调用了_PyCFunction_FastCallDict()
,这个CFunction
当然就是我们的eval,后面就进入到了builtin_eval()
的执行当中
PyObject *
_PyObject_FastCallDict(PyObject *callable, PyObject *const *args, Py_ssize_t nargs,
PyObject *kwargs)
{
/* _PyObject_FastCallDict() must not be called with an exception set,
because it can clear it (directly or indirectly) and so the
caller loses its exception */
assert(!PyErr_Occurred());
assert(callable != NULL);
assert(nargs >= 0);
assert(nargs == 0 || args != NULL);
assert(kwargs == NULL || PyDict_Check(kwargs));
if (PyFunction_Check(callable)) {
return _PyFunction_FastCallDict(callable, args, nargs, kwargs);
}
else if (PyCFunction_Check(callable)) {
return _PyCFunction_FastCallDict(callable, args, nargs, kwargs);
}
所以呢?unbound=0
是怎么来的?让我们看下lookup_maybe_method
干了什么。
static PyObject *
lookup_maybe_method(PyObject *self, _Py_Identifier *attrid, int *unbound)
{
PyObject *res = _PyType_LookupId(Py_TYPE(self), attrid);
//res = eval()
//这里就是把eval从__eq__里取出来,这里的attrid就是__eq__
if (res == NULL) {
return NULL;
}
if (PyFunction_Check(res)) {
/* Avoid temporary PyMethodObject */
*unbound = 1;
Py_INCREF(res);
}
else {
*unbound = 0;
descrgetfunc f = Py_TYPE(res)->tp_descr_get;
// descr descriptor tp_descr_get是获取新式类里的__get__方法
// 在python中,如果一个新式类定义了__get__, __set__, __delete__方法中的一个或者多个,那么这里的descriptor指的是所定义__get__, __set__, __delete__
if (f == NULL) {
Py_INCREF(res);
//引用计数器 +1
}
else {
res = f(res, self, (PyObject *)(Py_TYPE(self)));
}
}
return res;
}
PyFunction_Check
相关宏定义:
#define PyFunction_Check(op) (Py_TYPE(op) == &PyFunction_Type)
#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
&PyFunction_Type
可以理解为PyFunction_Type[0]
,PyFunction_Type数组:
PyTypeObject PyFunction_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"function",
sizeof(PyFunctionObject),
0,
(destructor)func_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
(reprfunc)func_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
function_call, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
func_new__doc__, /* tp_doc */
(traverseproc)func_traverse, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
offsetof(PyFunctionObject, func_weakreflist), /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
0, /* tp_methods */
func_memberlist, /* tp_members */
func_getsetlist, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
func_descr_get, /* tp_descr_get */
0, /* tp_descr_set */
offsetof(PyFunctionObject, func_dict), /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
func_new, /* tp_new */
};
PyVarObject_HEAD_INIT(&PyType_Type, 0) "function"
前面的那堆是类型转换,不管他。
这里的意思是ob_type
得是"function"
才能让PyFunction_Check
返回1
,因为eval
的ob_type
是builtin_function_or_method
,所以会返回0
可以通过如下简单测试验证,如下例子中的ob_type
为function
,并且返回的unbound = 1
:
def hello(aa, bb):
print(aa, bb)
a.__class__.__eq__ = hello
然后我们明显没有定义class A
的__get__
,所以descrgetfunc = NULL
,之后lookup_maybe_method
结束,就把eval
返回过来了顺带确定unbound = 0
总结
在web安全的学习当中,很多语言的小trick看似普通,但是,我们要弄明白它的原理,因为了解trick背后的原理更有收获比起光光记住小trick,往往来得更有收获。
阅读源码时往往可以参考官方的文档,这样便于理解其设计理念,知道了整体架构,也便于后续的分析。